refactor: implement control plane as controller-runtime manager
Some checks failed
Tests / Run on Ubuntu (push) Waiting to run
Lint / Run on Ubuntu (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled

This commit is contained in:
Peter 2025-01-20 17:06:41 +01:00
parent a5c170a478
commit 3104f50c58
Signed by: prskr
GPG key ID: F56BED6903BC5E37
67 changed files with 3693 additions and 261 deletions

View file

@ -27,6 +27,7 @@ linters:
- gocyclo
- gofmt
- goimports
- goheader
- gosimple
- godox
- govet
@ -56,6 +57,12 @@ linters-settings:
- dot
goimports:
local-prefixes: code.icb4dc0.de/prskr/supabase-operator
goheader:
values:
const:
AUTHOR: Peter Kurfer
template-path: hack/header.tmpl
importas:
no-unaliased: true
no-extra-aliases: true

View file

@ -47,4 +47,13 @@ resources:
defaulting: true
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: k8s.icb4dc0.de
group: supabase
kind: Storage
path: code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1
version: v1alpha1
version: "3"

View file

@ -1,5 +1,5 @@
/*
Copyright 2024 Peter Kurfer.
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -54,15 +54,30 @@ type APIGatewaySpec struct {
Envoy *EnvoySpec `json:"envoy"`
// JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs
JWKSSelector *corev1.SecretKeySelector `json:"jwks"`
// ServiceSelector - selector to match all Supabase services (or in fact EndpointSlices) that should be considered for this APIGateway
// +kubebuilder:default={"matchExpressions":{{"key": "app.kubernetes.io/part-of", "operator":"In", "values":{"supabase"}},{"key":"supabase.k8s.icb4dc0.de/api-gateway-target","operator":"Exists"}}}
ServiceSelector *metav1.LabelSelector `json:"serviceSelector"`
// ComponentTypeLabel - Label to identify which Supabase component a Service represents (e.g. auth, postgrest, ...)
// +kubebuilder:default="app.kubernetes.io/name"
ComponentTypeLabel string `json:"componentTypeLabel,omitempty"`
}
type EnvoyStatus struct {
ConfigVersion string `json:"configVersion,omitempty"`
ResourceHash []byte `json:"resourceHash,omitempty"`
}
// APIGatewayStatus defines the observed state of APIGateway.
type APIGatewayStatus struct{}
type APIGatewayStatus struct {
Envoy EnvoyStatus `json:"envoy,omitempty"`
ServiceTargets map[string][]string `json:"serviceTargets,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// APIGateway is the Schema for the apigateways API.
// +kubebuilder:printcolumn:name="EnvoyConfigVersion",type=string,JSONPath=`.status.envoy.configVersion`
type APIGateway struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

View file

@ -0,0 +1,64 @@
/*
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// StorageSpec defines the desired state of Storage.
type StorageSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// Foo is an example field of Storage. Edit storage_types.go to remove/update
Foo string `json:"foo,omitempty"`
}
// StorageStatus defines the observed state of Storage.
type StorageStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// Storage is the Schema for the storages API.
type Storage struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec StorageSpec `json:"spec,omitempty"`
Status StorageStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// StorageList contains a list of Storage.
type StorageList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Storage `json:"items"`
}
func init() {
SchemeBuilder.Register(&Storage{}, &StorageList{})
}

View file

@ -1,7 +1,7 @@
//go:build !ignore_autogenerated
/*
Copyright 2024 Peter Kurfer.
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ package v1alpha1
import (
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@ -31,7 +32,7 @@ func (in *APIGateway) DeepCopyInto(out *APIGateway) {
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
out.Status = in.Status
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIGateway.
@ -97,6 +98,11 @@ func (in *APIGatewaySpec) DeepCopyInto(out *APIGatewaySpec) {
*out = new(v1.SecretKeySelector)
(*in).DeepCopyInto(*out)
}
if in.ServiceSelector != nil {
in, out := &in.ServiceSelector, &out.ServiceSelector
*out = new(metav1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIGatewaySpec.
@ -112,6 +118,23 @@ func (in *APIGatewaySpec) DeepCopy() *APIGatewaySpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *APIGatewayStatus) DeepCopyInto(out *APIGatewayStatus) {
*out = *in
in.Envoy.DeepCopyInto(&out.Envoy)
if in.ServiceTargets != nil {
in, out := &in.ServiceTargets, &out.ServiceTargets
*out = make(map[string][]string, len(*in))
for key, val := range *in {
var outVal []string
if val == nil {
(*out)[key] = nil
} else {
inVal := (*in)[key]
in, out := &inVal, &outVal
*out = make([]string, len(*in))
copy(*out, *in)
}
(*out)[key] = outVal
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIGatewayStatus.
@ -764,6 +787,26 @@ func (in *EnvoySpec) DeepCopy() *EnvoySpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *EnvoyStatus) DeepCopyInto(out *EnvoyStatus) {
*out = *in
if in.ResourceHash != nil {
in, out := &in.ResourceHash, &out.ResourceHash
*out = make([]byte, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyStatus.
func (in *EnvoyStatus) DeepCopy() *EnvoyStatus {
if in == nil {
return nil
}
out := new(EnvoyStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GithubAuthProvider) DeepCopyInto(out *GithubAuthProvider) {
*out = *in
@ -903,6 +946,95 @@ func (in *PostgrestSpec) DeepCopy() *PostgrestSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Storage) DeepCopyInto(out *Storage) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
out.Status = in.Status
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage.
func (in *Storage) DeepCopy() *Storage {
if in == nil {
return nil
}
out := new(Storage)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Storage) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StorageList) DeepCopyInto(out *StorageList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Storage, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageList.
func (in *StorageList) DeepCopy() *StorageList {
if in == nil {
return nil
}
out := new(StorageList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *StorageList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StorageSpec) DeepCopyInto(out *StorageSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageSpec.
func (in *StorageSpec) DeepCopy() *StorageSpec {
if in == nil {
return nil
}
out := new(StorageSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StorageStatus) DeepCopyInto(out *StorageStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageStatus.
func (in *StorageStatus) DeepCopy() *StorageStatus {
if in == nil {
return nil
}
out := new(StorageStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StudioSpec) DeepCopyInto(out *StudioSpec) {
*out = *in

View file

@ -1,5 +1,5 @@
/*
Copyright 2024 Peter Kurfer.
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,12 +18,11 @@ package main
import (
"context"
"errors"
"crypto/tls"
"fmt"
"net"
"time"
"github.com/alecthomas/kong"
clusterservice "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
discoverygrpc "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
endpointservice "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
@ -31,7 +30,7 @@ import (
routeservice "github.com/envoyproxy/go-control-plane/envoy/service/route/v3"
runtimeservice "github.com/envoyproxy/go-control-plane/envoy/service/runtime/v3"
secretservice "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3"
"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
"github.com/envoyproxy/go-control-plane/pkg/server/v3"
"google.golang.org/grpc"
grpchealth "google.golang.org/grpc/health"
@ -39,16 +38,103 @@ import (
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/reflection"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
mgr "sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"code.icb4dc0.de/prskr/supabase-operator/internal/controlplane"
)
//nolint:lll // flag declaration with struct tags is as long as it is
type controlPlane struct {
ListenAddr string `name:"listen-address" default:":18000" help:"The address the control plane binds to."`
ListenAddr string `name:"listen-address" default:":18000" help:"The address the control plane binds to."`
MetricsAddr string `name:"metrics-bind-address" default:"0" help:"The address the metrics endpoint binds to. Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service."`
EnableLeaderElection bool `name:"leader-elect" default:"false" help:"Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager."`
ProbeAddr string `name:"health-probe-bind-address" default:":8081" help:"The address the probe endpoint binds to."`
SecureMetrics bool `name:"metrics-secure" default:"true" help:"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead."`
EnableHTTP2 bool `name:"enable-http2" default:"false" help:"If set, HTTP/2 will be enabled for the metrics and webhook servers"`
}
func (p controlPlane) Run(ctx context.Context, cache cache.SnapshotCache) (err error) {
func (cp controlPlane) Run(ctx context.Context) error {
var tlsOpts []func(*tls.Config)
// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("disabling http/2")
c.NextProtos = []string{"http/1.1"}
}
if !cp.EnableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: cp.MetricsAddr,
SecureServing: cp.SecureMetrics,
TLSOpts: tlsOpts,
}
if cp.SecureMetrics {
// FilterProvider is used to protect the metrics endpoint with authn/authz.
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
HealthProbeBindAddress: cp.ProbeAddr,
LeaderElection: cp.EnableLeaderElection,
BaseContext: func() context.Context { return ctx },
LeaderElectionID: "30f6fafb.k8s.icb4dc0.de",
LeaderElectionReleaseOnCancel: true,
})
if err != nil {
return fmt.Errorf("unable to start control plane: %w", err)
}
envoySnapshotCache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, nil)
envoySrv, err := cp.envoyServer(ctx, envoySnapshotCache)
if err != nil {
return err
}
if err := mgr.Add(envoySrv); err != nil {
return fmt.Errorf("failed to add enovy server to manager: %w", err)
}
if err = (&controlplane.APIGatewayReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Cache: envoySnapshotCache,
}).SetupWithManager(mgr); err != nil {
return fmt.Errorf("unable to create controller Core DB: %w", err)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctx); err != nil {
return fmt.Errorf("problem running manager: %w", err)
}
return nil
}
func (cp controlPlane) envoyServer(
ctx context.Context,
cache cachev3.SnapshotCache,
) (runnable mgr.Runnable, err error) {
const (
grpcKeepaliveTime = 30 * time.Second
grpcKeepaliveTimeout = 5 * time.Second
@ -56,19 +142,10 @@ func (p controlPlane) Run(ctx context.Context, cache cache.SnapshotCache) (err e
grpcMaxConcurrentStreams = 1000000
)
logger := ctrl.Log.WithName("control-plane")
clientOpts := client.Options{
Scheme: scheme,
}
logger.Info("Creating client")
watcherClient, err := client.NewWithWatch(ctrl.GetConfigOrDie(), clientOpts)
if err != nil {
return err
}
srv := server.NewServer(ctx, cache, nil)
var (
logger = ctrl.Log.WithName("control-plane")
srv = server.NewServer(ctx, cache, nil)
)
// gRPC golang library sets a very small upper bound for the number gRPC/h2
// streams over a single TCP connection. If a proxy multiplexes requests over
@ -89,13 +166,14 @@ func (p controlPlane) Run(ctx context.Context, cache cache.SnapshotCache) (err e
)
grpcServer := grpc.NewServer(grpcOptions...)
logger.Info("Opening listener", "addr", p.ListenAddr)
lis, err := net.Listen("tcp", p.ListenAddr)
logger.Info("Opening listener", "addr", cp.ListenAddr)
lis, err := net.Listen("tcp", cp.ListenAddr)
if err != nil {
return fmt.Errorf("opening listener: %w", err)
return nil, fmt.Errorf("opening listener: %w", err)
}
logger.Info("Preparing health endpoints")
healthService := grpchealth.NewServer()
healthService.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
@ -109,39 +187,11 @@ func (p controlPlane) Run(ctx context.Context, cache cache.SnapshotCache) (err e
runtimeservice.RegisterRuntimeDiscoveryServiceServer(grpcServer, srv)
grpc_health_v1.RegisterHealthServer(grpcServer, healthService)
// discoverygrpc.AggregatedDiscoveryService_ServiceDesc.ServiceName
endpointsController := controlplane.EndpointsController{
Client: watcherClient,
Cache: cache,
}
errOut := make(chan error)
go func(errOut chan<- error) {
logger.Info("Starting gRPC server")
errOut <- grpcServer.Serve(lis)
}(errOut)
go func(errOut chan<- error) {
logger.Info("Staring endpoints controller")
errOut <- endpointsController.Run(ctx)
}(errOut)
go func(errOut chan error) {
for out := range errOut {
err = errors.Join(err, out)
}
}(errOut)
<-ctx.Done()
grpcServer.Stop()
return err
}
//nolint:unparam // signature required by kong
func (p controlPlane) AfterApply(kongctx *kong.Context) error {
kongctx.BindTo(cache.NewSnapshotCache(false, cache.IDHash{}, nil), (*cache.SnapshotCache)(nil))
return nil
return mgr.RunnableFunc(func(ctx context.Context) error {
go func(ctx context.Context) {
<-ctx.Done()
grpcServer.GracefulStop()
}(ctx)
return grpcServer.Serve(lis)
}), nil
}

View file

@ -87,23 +87,14 @@ func (m manager) Run(ctx context.Context) error {
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: m.ProbeAddr,
LeaderElection: m.EnableLeaderElection,
LeaderElectionID: "05f9463f.k8s.icb4dc0.de",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: m.ProbeAddr,
LeaderElection: m.EnableLeaderElection,
BaseContext: func() context.Context { return ctx },
LeaderElectionID: "05f9463f.k8s.icb4dc0.de",
LeaderElectionReleaseOnCancel: true,
})
if err != nil {
return fmt.Errorf("unable to start manager: %w", err)

View file

@ -14,7 +14,11 @@ spec:
singular: apigateway
scope: Namespaced
versions:
- name: v1alpha1
- additionalPrinterColumns:
- jsonPath: .status.envoy.configVersion
name: EnvoyConfigVersion
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: APIGateway is the Schema for the apigateways API.
@ -39,6 +43,11 @@ spec:
spec:
description: APIGatewaySpec defines the desired state of APIGateway.
properties:
componentTypeLabel:
default: app.kubernetes.io/name
description: ComponentTypeLabel - Label to identify which Supabase
component a Service represents (e.g. auth, postgrest, ...)
type: string
envoy:
description: Envoy - configure the envoy instance and most importantly
the control-plane
@ -61,6 +70,12 @@ spec:
- host
- port
type: object
nodeName:
description: |-
NodeName - identifies the Envoy cluster within the current namespace
if not set, the name of the APIGateway resource will be used
The primary use case is to make the assignment of multiple supabase instances in a single namespace explicit.
type: string
workloadTemplate:
description: WorkloadTemplate - customize the Envoy deployment
properties:
@ -776,12 +791,83 @@ spec:
- key
type: object
x-kubernetes-map-type: atomic
serviceSelector:
default:
matchExpressions:
- key: app.kubernetes.io/part-of
operator: In
values:
- supabase
- key: supabase.k8s.icb4dc0.de/api-gateway-target
operator: Exists
description: ServiceSelector - selector to match all Supabase services
(or in fact EndpointSlices) that should be considered for this APIGateway
properties:
matchExpressions:
description: matchExpressions is a list of label selector requirements.
The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector applies
to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
required:
- envoy
- jwks
- serviceSelector
type: object
status:
description: APIGatewayStatus defines the observed state of APIGateway.
properties:
envoy:
properties:
configVersion:
type: string
resourceHash:
format: byte
type: string
type: object
serviceTargets:
additionalProperties:
items:
type: string
type: array
type: object
type: object
type: object
served: true

View file

@ -0,0 +1,54 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.5
name: storages.supabase.k8s.icb4dc0.de
spec:
group: supabase.k8s.icb4dc0.de
names:
kind: Storage
listKind: StorageList
plural: storages
singular: storage
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Storage is the Schema for the storages API.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: StorageSpec defines the desired state of Storage.
properties:
foo:
description: Foo is an example field of Storage. Edit storage_types.go
to remove/update
type: string
type: object
status:
description: StorageStatus defines the observed state of Storage.
type: object
type: object
served: true
storage: true
subresources:
status: {}

View file

@ -5,6 +5,7 @@ resources:
- bases/supabase.k8s.icb4dc0.de_cores.yaml
- bases/supabase.k8s.icb4dc0.de_apigateways.yaml
- bases/supabase.k8s.icb4dc0.de_dashboards.yaml
- bases/supabase.k8s.icb4dc0.de_storages.yaml
# +kubebuilder:scaffold:crdkustomizeresource
patches:

View file

@ -4,6 +4,23 @@ kind: ClusterRole
metadata:
name: control-plane-role
rules:
- apiGroups:
- supabase.k8s.icb4dc0.de
resources:
- apigateways
verbs:
- get
- list
- watch
- apiGroups:
- supabase.k8s.icb4dc0.de
resources:
- apigateways/status
verbs:
- get
- patch
- update
- apiGroups:
- discovery.k8s.io
resources:

View file

@ -36,3 +36,10 @@ resources:
# if you do not want those helpers be installed with your Project.
- dashboard_editor_role.yaml
- dashboard_viewer_role.yaml
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
# default, aiding admins in cluster management. Those roles are
# not used by the {{ .ProjectName }} itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
- storage_admin_role.yaml
- storage_editor_role.yaml
- storage_viewer_role.yaml

View file

@ -13,3 +13,6 @@ subjects:
- kind: ServiceAccount
name: controller-manager
namespace: supabase-system
- kind: ServiceAccount
name: control-plane
namespace: supabase-system

View file

@ -42,6 +42,7 @@ rules:
- apigateways
- cores
- dashboards
- storages
verbs:
- create
- delete
@ -56,6 +57,7 @@ rules:
- apigateways/finalizers
- cores/finalizers
- dashboards/finalizers
- storages/finalizers
verbs:
- update
- apiGroups:
@ -64,6 +66,7 @@ rules:
- apigateways/status
- cores/status
- dashboards/status
- storages/status
verbs:
- get
- patch

View file

@ -0,0 +1,27 @@
# This rule is not used by the project supabase-operator itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over supabase.k8s.icb4dc0.de.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: storage-admin-role
rules:
- apiGroups:
- supabase.k8s.icb4dc0.de
resources:
- storages
verbs:
- '*'
- apiGroups:
- supabase.k8s.icb4dc0.de
resources:
- storages/status
verbs:
- get

View file

@ -0,0 +1,33 @@
# This rule is not used by the project supabase-operator itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the supabase.k8s.icb4dc0.de.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: storage-editor-role
rules:
- apiGroups:
- supabase.k8s.icb4dc0.de
resources:
- storages
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- supabase.k8s.icb4dc0.de
resources:
- storages/status
verbs:
- get

View file

@ -0,0 +1,29 @@
# This rule is not used by the project supabase-operator itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to supabase.k8s.icb4dc0.de resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: storage-viewer-role
rules:
- apiGroups:
- supabase.k8s.icb4dc0.de
resources:
- storages
verbs:
- get
- list
- watch
- apiGroups:
- supabase.k8s.icb4dc0.de
resources:
- storages/status
verbs:
- get

View file

@ -8,4 +8,5 @@ resources:
- supabase_v1alpha1_core.yaml
- supabase_v1alpha1_apigateway.yaml
- supabase_v1alpha1_dashboard.yaml
- supabase_v1alpha1_storage.yaml
# +kubebuilder:scaffold:manifestskustomizesamples

View file

@ -0,0 +1,9 @@
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
kind: Storage
metadata:
labels:
app.kubernetes.io/name: supabase-operator
app.kubernetes.io/managed-by: kustomize
name: storage-sample
spec:
# TODO(user): Add fields here

View file

@ -1,5 +1,5 @@
/*
Copyright 2024 Peter Kurfer.
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

13
hack/header.tmpl Normal file
View file

@ -0,0 +1,13 @@
Copyright {{ YEAR }} {{ AUTHOR }}.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -1,5 +1,5 @@
/*
Copyright 2024 Peter Kurfer.
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -1,5 +1,5 @@
/*
Copyright 2024 Peter Kurfer.
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -221,8 +221,8 @@ func (r *CoreAuthReconciler) reconcileAuthService(
core.Labels,
)
if _, ok := authService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
authService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
if _, ok := authService.Labels[meta.SupabaseLabel.ApiGatewayTarget]; !ok {
authService.Labels[meta.SupabaseLabel.ApiGatewayTarget] = core.Name
}
authService.Spec = corev1.ServiceSpec{

View file

@ -228,8 +228,8 @@ func (r *CorePostgrestReconiler) reconcilePostgrestService(
core.Labels,
)
if _, ok := postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
postgrestService.Labels[meta.SupabaseLabel.EnvoyCluster] = core.Name
if _, ok := postgrestService.Labels[meta.SupabaseLabel.ApiGatewayTarget]; !ok {
postgrestService.Labels[meta.SupabaseLabel.ApiGatewayTarget] = ""
}
postgrestService.Spec = corev1.ServiceSpec{

View file

@ -196,8 +196,8 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaService(
dashboard.Labels,
)
if _, ok := pgMetaService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
pgMetaService.Labels[meta.SupabaseLabel.EnvoyCluster] = dashboard.Name
if _, ok := pgMetaService.Labels[meta.SupabaseLabel.ApiGatewayTarget]; !ok {
pgMetaService.Labels[meta.SupabaseLabel.ApiGatewayTarget] = ""
}
pgMetaService.Spec = corev1.ServiceSpec{

View file

@ -218,8 +218,8 @@ func (r *DashboardStudioReconciler) reconcileStudioService(
dashboard.Labels,
)
if _, ok := studioService.Labels[meta.SupabaseLabel.EnvoyCluster]; !ok {
studioService.Labels[meta.SupabaseLabel.EnvoyCluster] = dashboard.Name
if _, ok := studioService.Labels[meta.SupabaseLabel.ApiGatewayTarget]; !ok {
studioService.Labels[meta.SupabaseLabel.ApiGatewayTarget] = ""
}
studioService.Spec = corev1.ServiceSpec{

View file

@ -1,3 +1,19 @@
// /*
// Copyright 2025 Peter Kurfer.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// */
package controller
import (

View file

@ -0,0 +1,63 @@
/*
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
)
// StorageReconciler reconciles a Storage object
type StorageReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=supabase.k8s.icb4dc0.de,resources=storages/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Storage object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.4/pkg/reconcile
func (r *StorageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *StorageReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&supabasev1alpha1.Storage{}).
Named("storage").
Complete(r)
}

View file

@ -0,0 +1,84 @@
/*
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
)
var _ = Describe("Storage Controller", func() {
Context("When reconciling a resource", func() {
const resourceName = "test-resource"
ctx := context.Background()
typeNamespacedName := types.NamespacedName{
Name: resourceName,
Namespace: "default", // TODO(user):Modify as needed
}
storage := &supabasev1alpha1.Storage{}
BeforeEach(func() {
By("creating the custom resource for the Kind Storage")
err := k8sClient.Get(ctx, typeNamespacedName, storage)
if err != nil && errors.IsNotFound(err) {
resource := &supabasev1alpha1.Storage{
ObjectMeta: metav1.ObjectMeta{
Name: resourceName,
Namespace: "default",
},
// TODO(user): Specify other spec details if needed.
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
})
AfterEach(func() {
// TODO(user): Cleanup logic after each test, like removing the resource instance.
resource := &supabasev1alpha1.Storage{}
err := k8sClient.Get(ctx, typeNamespacedName, resource)
Expect(err).NotTo(HaveOccurred())
By("Cleanup the specific resource instance Storage")
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
controllerReconciler := &StorageReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
}
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
NamespacedName: typeNamespacedName,
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.
})
})
})

View file

@ -0,0 +1,197 @@
/*
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controlplane
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
discoveryv1 "k8s.io/api/discovery/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
)
// APIGatewayReconciler reconciles a APIGateway object
type APIGatewayReconciler struct {
client.Client
Scheme *runtime.Scheme
Cache cachev3.SnapshotCache
}
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile
func (r *APIGatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
var (
gateway supabasev1alpha1.APIGateway
logger = log.FromContext(ctx)
endpointSliceList discoveryv1.EndpointSliceList
)
logger.Info("Reconciling APIGateway")
if err := r.Get(ctx, req.NamespacedName, &gateway); client.IgnoreNotFound(err) != nil {
logger.Error(err, "unable to fetch Gateway")
return ctrl.Result{}, err
}
selector, err := metav1.LabelSelectorAsSelector(gateway.Spec.ServiceSelector)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to create selector for EndpointSlices: %w", err)
}
if err := r.List(ctx, &endpointSliceList, client.MatchingLabelsSelector{Selector: selector}); err != nil {
return ctrl.Result{}, err
}
services := EnvoyServices{ServiceLabelKey: gateway.Spec.ComponentTypeLabel}
services.UpsertEndpointSlices(endpointSliceList.Items...)
rawServices, err := json.Marshal(services)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to prepare config hash: %w", err)
}
serviceHash := sha256.New().Sum(rawServices)
if bytes.Equal(serviceHash, gateway.Status.Envoy.ResourceHash) {
logger.Info("Resource hash did not change - skipping reconciliation")
return ctrl.Result{}, nil
}
logger.Info("Updating service targets")
_, err = controllerutil.CreateOrPatch(ctx, r.Client, &gateway, func() error {
gateway.Status.ServiceTargets = services.Targets()
gateway.Status.Envoy.ConfigVersion = strconv.FormatInt(time.Now().UTC().UnixMilli(), 10)
gateway.Status.Envoy.ResourceHash = serviceHash
return nil
})
if err != nil {
return ctrl.Result{}, err
}
instance := fmt.Sprintf("%s:%s", gateway.Spec.Envoy.NodeName, gateway.Namespace)
logger.Info("Computing Envoy snapshot for current service targets", "version", gateway.Status.Envoy.ConfigVersion)
snapshot, err := services.snapshot(ctx, instance, gateway.Status.Envoy.ConfigVersion)
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to prepare snapshot: %w", err)
}
logger.Info("Propagating Envoy snapshot", "version", gateway.Status.Envoy.ConfigVersion)
if err := r.Cache.SetSnapshot(ctx, instance, snapshot); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to propagate snapshot: %w", err)
}
return ctrl.Result{}, nil
}
func (r *APIGatewayReconciler) SetupWithManager(mgr ctrl.Manager) error {
gatewayTargetLabelSelector, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{{
Key: meta.SupabaseLabel.ApiGatewayTarget,
Operator: metav1.LabelSelectorOpExists,
}},
})
if err != nil {
return fmt.Errorf("failed to build gateway target predicate: %w", err)
}
return ctrl.NewControllerManagedBy(mgr).
For(new(supabasev1alpha1.APIGateway)).
Watches(
new(discoveryv1.EndpointSlice),
r.endpointSliceEventHandler(),
builder.WithPredicates(gatewayTargetLabelSelector)).
Complete(r)
}
// endpointSliceEventHandler - prepares an event handler that checks whether the EndpointSlice has a specific target
// or if it is targeting the only APIGateway in its namespace (default behavior for the operator)
func (r *APIGatewayReconciler) endpointSliceEventHandler() handler.TypedEventHandler[client.Object, reconcile.Request] {
return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
var (
logger = log.FromContext(ctx)
apiGatewayList supabasev1alpha1.APIGatewayList
)
endpointSlice, ok := obj.(*discoveryv1.EndpointSlice)
if !ok {
logger.Info("Cannot map event to reconcile request, because object has unexpected type", "type", fmt.Sprintf("%T", obj))
return nil
}
if err := r.Client.List(ctx, &apiGatewayList, client.InNamespace(endpointSlice.Namespace)); err != nil {
logger.Error(err, "failed to list APIGateways to determine reconcile targets")
return nil
}
target, ok := endpointSlice.Labels[meta.SupabaseLabel.ApiGatewayTarget]
if !ok {
// should not happen, just to be sure
return nil
}
var reconcileRequests []reconcile.Request
if target != "" {
for _, gw := range apiGatewayList.Items {
if strings.EqualFold(gw.Spec.Envoy.NodeName, target) {
reconcileRequests = append(reconcileRequests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: gw.Name,
Namespace: gw.Namespace,
},
})
}
}
} else {
reconcileRequests = make([]reconcile.Request, 0, len(apiGatewayList.Items))
for _, gw := range apiGatewayList.Items {
reconcileRequests = append(reconcileRequests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: gw.Name,
Namespace: gw.Namespace,
},
})
}
}
return reconcileRequests
})
}

View file

@ -1,6 +1,10 @@
package controlplane
import (
"encoding/json"
"fmt"
"slices"
"strings"
"time"
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
@ -10,11 +14,28 @@ import (
discoveryv1 "k8s.io/api/discovery/v1"
)
var _ json.Marshaler = (*ServiceCluster)(nil)
type ServiceCluster struct {
ServiceEndpoints map[string]Endpoints
}
func (c *ServiceCluster) AddOrUpdateEndpoints(eps *discoveryv1.EndpointSlice) {
// MarshalJSON implements json.Marshaler.
func (c *ServiceCluster) MarshalJSON() ([]byte, error) {
tmp := struct {
Endpoints []string `json:"endpoints"`
}{}
for _, endpoints := range c.ServiceEndpoints {
tmp.Endpoints = append(tmp.Endpoints, endpoints.Targets...)
}
slices.Sort(tmp.Endpoints)
return json.Marshal(tmp)
}
func (c *ServiceCluster) AddOrUpdateEndpoints(eps discoveryv1.EndpointSlice) {
if c.ServiceEndpoints == nil {
c.ServiceEndpoints = make(map[string]Endpoints)
}
@ -22,6 +43,16 @@ func (c *ServiceCluster) AddOrUpdateEndpoints(eps *discoveryv1.EndpointSlice) {
c.ServiceEndpoints[eps.Name] = newEndpointsFromSlice(eps)
}
func (c ServiceCluster) Targets() []string {
var targets []string
for _, ep := range c.ServiceEndpoints {
targets = append(targets, ep.Targets...)
}
return targets
}
func (c ServiceCluster) Cluster(name string, port uint32) *clusterv3.Cluster {
return &clusterv3.Cluster{
Name: name,
@ -47,12 +78,13 @@ func (c ServiceCluster) endpoints(port uint32) []*endpointv3.LocalityLbEndpoints
return eps
}
func newEndpointsFromSlice(eps *discoveryv1.EndpointSlice) Endpoints {
func newEndpointsFromSlice(eps discoveryv1.EndpointSlice) Endpoints {
var result Endpoints
for _, ep := range eps.Endpoints {
if ep.Conditions.Ready != nil && *ep.Conditions.Ready {
result.Addresses = append(result.Addresses, ep.Addresses...)
result.Targets = append(result.Targets, strings.ToLower(fmt.Sprintf("%s/%s", ep.TargetRef.Kind, ep.TargetRef.Name)))
}
}
@ -61,6 +93,7 @@ func newEndpointsFromSlice(eps *discoveryv1.EndpointSlice) Endpoints {
type Endpoints struct {
Addresses []string
Targets []string
}
func (e Endpoints) LBEndpoints(port uint32) []*endpointv3.LbEndpoint {

View file

@ -1,29 +1,8 @@
/*
Copyright 2024 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controlplane
import (
"context"
"errors"
"fmt"
"slices"
"strconv"
"sync"
"time"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
@ -35,171 +14,64 @@ import (
"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
"google.golang.org/protobuf/types/known/anypb"
discoveryv1 "k8s.io/api/discovery/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/watch"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
)
var (
ErrUnexpectedObject = errors.New("unexpected object")
ErrNoEnvoyClusterLabel = errors.New("no Envoy cluster label set")
supabaseServices = []string{
supabase.ServiceConfig.Postgrest.Name,
supabase.ServiceConfig.Auth.Name,
supabase.ServiceConfig.PGMeta.Name,
}
)
type EndpointsController struct {
lock sync.Mutex
Client client.WithWatch
Cache cache.SnapshotCache
envoyClusters map[string]*envoyClusterServices
type EnvoyServices struct {
ServiceLabelKey string `json:"-"`
Postgrest *PostgrestCluster `json:"postgrest,omitempty"`
GoTrue *GoTrueCluster `json:"auth,omitempty"`
PGMeta *PGMetaCluster `json:"pgmeta,omitempty"`
Studio *StudioCluster `json:"studio,omitempty"`
}
func (c *EndpointsController) Run(ctx context.Context) error {
var (
logger = ctrl.Log.WithName("endpoints-controller")
endpointSlices discoveryv1.EndpointSliceList
)
selector := labels.NewSelector()
partOfRequirement, err := labels.NewRequirement(meta.WellKnownLabel.PartOf, selection.Equals, []string{"supabase"})
if err != nil {
return fmt.Errorf("preparing watcher selectors: %w", err)
}
nameRequirement, err := labels.NewRequirement(meta.WellKnownLabel.Name, selection.In, supabaseServices)
if err != nil {
return fmt.Errorf("preparing watcher selectors: %w", err)
}
envoyClusterRequirement, err := labels.NewRequirement(meta.SupabaseLabel.EnvoyCluster, selection.Exists, nil)
if err != nil {
return fmt.Errorf("preparing watcher selectors: %w", err)
}
selector.Add(*partOfRequirement, *nameRequirement, *envoyClusterRequirement)
watcher, err := c.Client.Watch(
ctx,
&endpointSlices,
client.MatchingLabelsSelector{
Selector: selector.Add(*partOfRequirement, *nameRequirement, *envoyClusterRequirement),
},
)
if err != nil {
return err
}
defer watcher.Stop()
for {
select {
case ev, more := <-watcher.ResultChan():
if !more {
return nil
func (s *EnvoyServices) UpsertEndpointSlices(endpointSlices ...discoveryv1.EndpointSlice) {
for _, eps := range endpointSlices {
switch eps.Labels[s.ServiceLabelKey] {
case supabase.ServiceConfig.Postgrest.Name:
if s.Postgrest == nil {
s.Postgrest = new(PostgrestCluster)
}
eventLogger := logger.WithValues("event_type", ev.Type)
switch ev.Type {
case watch.Added, watch.Modified:
eps, ok := ev.Object.(*discoveryv1.EndpointSlice)
if !ok {
logger.Error(fmt.Errorf("%w: %T", ErrUnexpectedObject, ev.Object), "expected EndpointSlice but got a different object type")
continue
}
if err := c.handleModificationEvent(log.IntoContext(ctx, eventLogger), eps); err != nil {
logger.Error(err, "error occurred during event handling")
}
s.Postgrest.AddOrUpdateEndpoints(eps)
case supabase.ServiceConfig.Auth.Name:
if s.GoTrue == nil {
s.GoTrue = new(GoTrueCluster)
}
case <-ctx.Done():
return ctx.Err()
s.GoTrue.AddOrUpdateEndpoints(eps)
case supabase.ServiceConfig.PGMeta.Name:
if s.PGMeta == nil {
s.PGMeta = new(PGMetaCluster)
}
s.PGMeta.AddOrUpdateEndpoints(eps)
}
}
}
func (c *EndpointsController) handleModificationEvent(ctx context.Context, epSlice *discoveryv1.EndpointSlice) error {
c.lock.Lock()
defer c.lock.Unlock()
func (s EnvoyServices) Targets() map[string][]string {
targets := make(map[string][]string)
var (
logger = log.FromContext(ctx)
instanceKey string
svc *envoyClusterServices
)
logger.Info("Observed endpoint slice", "name", epSlice.Name)
if c.envoyClusters == nil {
c.envoyClusters = make(map[string]*envoyClusterServices)
if s.Postgrest != nil {
targets[supabase.ServiceConfig.Postgrest.Name] = s.Postgrest.Targets()
}
envoyNodeName, ok := epSlice.Labels[meta.SupabaseLabel.EnvoyCluster]
if !ok {
return fmt.Errorf("%w: at object %s", ErrNoEnvoyClusterLabel, epSlice.Name)
if s.GoTrue != nil {
targets[supabase.ServiceConfig.Auth.Name] = s.GoTrue.Targets()
}
instanceKey = fmt.Sprintf("%s:%s", envoyNodeName, epSlice.Namespace)
if svc, ok = c.envoyClusters[instanceKey]; !ok {
svc = new(envoyClusterServices)
if s.PGMeta != nil {
targets[supabase.ServiceConfig.PGMeta.Name] = s.PGMeta.Targets()
}
svc.UpsertEndpoints(epSlice)
if s.Studio != nil {
targets[supabase.ServiceConfig.Studio.Name] = s.Studio.Targets()
}
c.envoyClusters[instanceKey] = svc
return c.updateSnapshot(ctx, instanceKey)
return targets
}
func (c *EndpointsController) updateSnapshot(ctx context.Context, instance string) error {
latestVersion := strconv.FormatInt(time.Now().UTC().UnixMilli(), 10)
snapshot, err := c.envoyClusters[instance].snapshot(instance, latestVersion)
if err != nil {
return err
}
return c.Cache.SetSnapshot(ctx, instance, snapshot)
}
type envoyClusterServices struct {
Postgrest *PostgrestCluster
GoTrue *GoTrueCluster
PGMeta *PGMetaCluster
Studio *StudioCluster
}
func (s *envoyClusterServices) UpsertEndpoints(eps *discoveryv1.EndpointSlice) {
switch eps.Labels[meta.WellKnownLabel.Name] {
case supabase.ServiceConfig.Postgrest.Name:
if s.Postgrest == nil {
s.Postgrest = new(PostgrestCluster)
}
s.Postgrest.AddOrUpdateEndpoints(eps)
case supabase.ServiceConfig.Auth.Name:
if s.GoTrue == nil {
s.GoTrue = new(GoTrueCluster)
}
s.GoTrue.AddOrUpdateEndpoints(eps)
case supabase.ServiceConfig.PGMeta.Name:
if s.PGMeta == nil {
s.PGMeta = new(PGMetaCluster)
}
s.PGMeta.AddOrUpdateEndpoints(eps)
}
}
func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapshot, error) {
func (s *EnvoyServices) snapshot(ctx context.Context, instance, version string) (*cache.Snapshot, error) {
const (
apiRouteName = "supabase"
studioRouteName = "supabas-studio"
@ -207,6 +79,8 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
listenerName = "supabase"
)
logger := log.FromContext(ctx)
apiConnectionManager := &hcm.HttpConnectionManager{
CodecType: hcm.HttpConnectionManager_AUTO,
StatPrefix: "http",
@ -327,6 +201,8 @@ func (s *envoyClusterServices) snapshot(instance, version string) (*cache.Snapsh
}}
if s.Studio != nil {
logger.Info("Adding studio listener")
listeners = append(listeners, &listenerv3.Listener{
Name: "studio",
Address: &corev3.Address{

View file

@ -39,9 +39,9 @@ var WellKnownLabel = struct {
}
var SupabaseLabel = struct {
Reload string
EnvoyCluster string
Reload string
ApiGatewayTarget string
}{
Reload: supabasev1alpha1.GroupVersion.Group + "/reload",
EnvoyCluster: supabasev1alpha1.GroupVersion.Group + "/envoy-cluster",
Reload: supabasev1alpha1.GroupVersion.Group + "/reload",
ApiGatewayTarget: supabasev1alpha1.GroupVersion.Group + "/api-gateway-target",
}

View file

@ -0,0 +1,27 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{1A6ACECB-D8D9-4613-B56E-8A4FEB6A78A0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "supabase-integration.api-test", "test\supabase-integration.api-test\supabase-integration.api-test.csproj", "{8C0E9D7E-5331-4AFB-919A-F00967971025}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8C0E9D7E-5331-4AFB-919A-F00967971025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8C0E9D7E-5331-4AFB-919A-F00967971025}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8C0E9D7E-5331-4AFB-919A-F00967971025}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8C0E9D7E-5331-4AFB-919A-F00967971025}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8C0E9D7E-5331-4AFB-919A-F00967971025} = {1A6ACECB-D8D9-4613-B56E-8A4FEB6A78A0}
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=e8b9e660_002Dfb4d_002D48ab_002D99f4_002D8949fff0e981/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="TestListTasks(SupabaseClientFixture)" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;
&lt;TestAncestor&gt;
&lt;TestId&gt;Testing Platform::8C0E9D7E-5331-4AFB-919A-F00967971025::net9.0::ServiceKeyTest&lt;/TestId&gt;
&lt;/TestAncestor&gt;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary>

View file

@ -0,0 +1,27 @@
using Supabase.Postgrest.Attributes;
using Supabase.Postgrest.Models;
namespace supabase_integration.api_test;
public class ServiceKeyTest
{
[Test]
[ClassDataSource<SupabaseClientFixture>(Shared = SharedType.PerAssembly)]
public async Task TestListTasks(SupabaseClientFixture fixture)
{
var resp = await fixture.ApiClient.Postgrest.Table<TaskList>().Get().ConfigureAwait(false);
await Assert.That(resp.Models.Count).IsGreaterThan(0);
}
}
[Table("lists")]
public class TaskList : BaseModel
{
[PrimaryKey("id")]
public int Id { get; set; }
[Column("user_id")]
public int UserId { get; set; }
[Column("name")]
public string Name { get; set; }
}

View file

@ -0,0 +1,22 @@
using Supabase;
using TUnit.Core.Interfaces;
namespace supabase_integration.api_test;
public class SupabaseClientFixture : IAsyncInitializer
{
public Task InitializeAsync()
{
ApiClient = new Client(
Environment.GetEnvironmentVariable("SUPBASE_URL") ?? "http://localhost:8000",
Environment.GetEnvironmentVariable("SUPBASE_ACCESS_KEY") ?? throw new ArgumentException("Supabase access key is missing."),
new SupabaseOptions
{
AutoConnectRealtime = false
}
);
return Task.CompletedTask;
}
public Client ApiClient { get; private set; }
}

View file

@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0")]

View file

@ -0,0 +1,16 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal static class SelfRegisteredExtensions
{
public static void AddSelfRegisteredExtensions(this global::Microsoft.Testing.Platform.Builder.ITestApplicationBuilder builder, string[] args)
{
Microsoft.Testing.Platform.MSBuild.TestingPlatformBuilderHook.AddExtensions(builder, args);
TUnit.Engine.Framework.TestingPlatformBuilderHook.AddExtensions(builder, args);
Microsoft.Testing.Extensions.CodeCoverage.TestingPlatformBuilderHook.AddExtensions(builder, args);
}
}

View file

@ -0,0 +1,19 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by Microsoft.Testing.Platform.MSBuild
// </auto-generated>
//------------------------------------------------------------------------------
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
internal sealed class TestingPlatformEntryPoint
{
public static async global::System.Threading.Tasks.Task<int> Main(string[] args)
{
global::Microsoft.Testing.Platform.Builder.ITestApplicationBuilder builder = await global::Microsoft.Testing.Platform.Builder.TestApplication.CreateBuilderAsync(args);
SelfRegisteredExtensions.AddSelfRegisteredExtensions(builder, args);
using (global::Microsoft.Testing.Platform.Builder.ITestApplication app = await builder.BuildAsync())
{
return await app.RunAsync();
}
}
}

View file

@ -0,0 +1,23 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("supabase-integration.api-test")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+a5c170a47816e50bce4b39b7d7b54764d92acb71")]
[assembly: System.Reflection.AssemblyProductAttribute("supabase-integration.api-test")]
[assembly: System.Reflection.AssemblyTitleAttribute("supabase-integration.api-test")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyMetadata("Microsoft.Testing.Platform.Application", "True")]
// Generated by the MSBuild WriteCodeFragment class.

View file

@ -0,0 +1 @@
c30ee9d790c053bb091f7b65a0618bb1bd275cc4b6492d6624dc720874d75514

View file

@ -0,0 +1,15 @@
is_global = true
build_property.TargetFramework = net9.0
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
build_property.InvariantGlobalization =
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = supabase_integration.api_test
build_property.ProjectDir = /Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 9.0
build_property.EnableCodeStyleSeverity =

View file

@ -0,0 +1,12 @@
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
global using global::TUnit.Assertions;
global using global::TUnit.Assertions.Extensions;
global using global::TUnit.Core;
global using static global::TUnit.Core.HookType;

View file

@ -0,0 +1 @@
18cd98c3087cd64be3c7db21ec9acf602d7e3393abf67ab6c148e18f5288dcac

View file

@ -0,0 +1,73 @@
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.csproj.AssemblyReference.cache
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.GeneratedMSBuildEditorConfig.editorconfig
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.AssemblyInfoInputs.cache
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.AssemblyInfo.cs
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.csproj.CoreCompileInputs.cache
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.gentestingplatformentrypointinputcache.cache
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.genautoregisteredextensionsinputcache.cache
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/AutoRegisteredExtensions.cs
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/TestPlatformEntryPoint.cs
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/supabase-integration.api-test
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/supabase-integration.api-test.deps.json
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/supabase-integration.api-test.runtimeconfig.json
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/supabase-integration.api-test.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/supabase-integration.api-test.pdb
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/EnumerableAsyncProcessor.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.Extensions.Logging.Abstractions.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.IdentityModel.Abstractions.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.IdentityModel.JsonWebTokens.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.IdentityModel.Logging.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.IdentityModel.Tokens.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.IO.RecyclableMemoryStream.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.Testing.Extensions.TrxReport.Abstractions.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.Testing.Platform.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Microsoft.Testing.Platform.MSBuild.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/MimeMapping.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Newtonsoft.Json.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Supabase.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Supabase.Core.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Supabase.Functions.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Supabase.Gotrue.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Supabase.Postgrest.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Supabase.Realtime.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Supabase.Storage.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/System.IdentityModel.Tokens.Jwt.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/System.Reactive.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/TUnit.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/TUnit.Assertions.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/TUnit.Core.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/TUnit.Engine.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/Websocket.Client.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/cs/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/de/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/es/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/fr/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/it/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/ja/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/ko/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/pl/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/pt-BR/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/ru/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/tr/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/zh-Hans/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/zh-Hant/Microsoft.Testing.Platform.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/cs/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/de/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/es/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/fr/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/it/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/ja/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/ko/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/pl/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/pt-BR/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/ru/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/tr/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/zh-Hans/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/bin/Debug/net9.0/zh-Hant/Microsoft.Testing.Platform.MSBuild.resources.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase.88E5058B.Up2Date
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/refint/supabase-integration.api-test.dll
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.pdb
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/supabase-integration.api-test.genruntimeconfig.cache
/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/Debug/net9.0/ref/supabase-integration.api-test.dll

View file

@ -0,0 +1 @@
ac7cea3191876b400be0355099ee75cb19980b90abdf6abdaa74befcf3d99901

View file

@ -0,0 +1 @@
24f9af8c04e70f6b6181eaa2f0ed1638767c3450d7387a035086bd4dfd85d781

View file

@ -0,0 +1 @@
ac7cea3191876b400be0355099ee75cb19980b90abdf6abdaa74befcf3d99901

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,47 @@
{
"version": 2,
"dgSpecHash": "R14E93Erk28=",
"success": true,
"projectFilePath": "/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/supabase-integration.api-test.csproj",
"expectedPackageFiles": [
"/Users/baez/.nuget/packages/enumerableasyncprocessor/2.0.6/enumerableasyncprocessor.2.0.6.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.diasymreader/2.0.0/microsoft.diasymreader.2.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/8.0.0/microsoft.extensions.dependencyinjection.abstractions.8.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.extensions.dependencymodel/6.0.1/microsoft.extensions.dependencymodel.6.0.1.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.extensions.logging.abstractions/8.0.0/microsoft.extensions.logging.abstractions.8.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.identitymodel.abstractions/7.5.1/microsoft.identitymodel.abstractions.7.5.1.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.identitymodel.jsonwebtokens/7.5.1/microsoft.identitymodel.jsonwebtokens.7.5.1.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.identitymodel.logging/7.5.1/microsoft.identitymodel.logging.7.5.1.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.identitymodel.tokens/7.5.1/microsoft.identitymodel.tokens.7.5.1.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.io.recyclablememorystream/3.0.0/microsoft.io.recyclablememorystream.3.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.testing.extensions.codecoverage/17.13.1/microsoft.testing.extensions.codecoverage.17.13.1.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.testing.extensions.trxreport.abstractions/1.4.3/microsoft.testing.extensions.trxreport.abstractions.1.4.3.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.testing.platform/1.4.3/microsoft.testing.platform.1.4.3.nupkg.sha512",
"/Users/baez/.nuget/packages/microsoft.testing.platform.msbuild/1.4.3/microsoft.testing.platform.msbuild.1.4.3.nupkg.sha512",
"/Users/baez/.nuget/packages/mimemapping/3.0.1/mimemapping.3.0.1.nupkg.sha512",
"/Users/baez/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg.sha512",
"/Users/baez/.nuget/packages/supabase/1.1.1/supabase.1.1.1.nupkg.sha512",
"/Users/baez/.nuget/packages/supabase.core/1.0.0/supabase.core.1.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/supabase.functions/2.0.0/supabase.functions.2.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/supabase.gotrue/6.0.3/supabase.gotrue.6.0.3.nupkg.sha512",
"/Users/baez/.nuget/packages/supabase.postgrest/4.0.3/supabase.postgrest.4.0.3.nupkg.sha512",
"/Users/baez/.nuget/packages/supabase.realtime/7.0.2/supabase.realtime.7.0.2.nupkg.sha512",
"/Users/baez/.nuget/packages/supabase.storage/2.0.2/supabase.storage.2.0.2.nupkg.sha512",
"/Users/baez/.nuget/packages/system.buffers/4.5.1/system.buffers.4.5.1.nupkg.sha512",
"/Users/baez/.nuget/packages/system.collections.immutable/8.0.0/system.collections.immutable.8.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/system.identitymodel.tokens.jwt/7.5.1/system.identitymodel.tokens.jwt.7.5.1.nupkg.sha512",
"/Users/baez/.nuget/packages/system.memory/4.5.4/system.memory.4.5.4.nupkg.sha512",
"/Users/baez/.nuget/packages/system.reactive/6.0.0/system.reactive.6.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/system.reflection.metadata/8.0.0/system.reflection.metadata.8.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/system.runtime.compilerservices.unsafe/6.0.0/system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/system.text.encodings.web/6.0.0/system.text.encodings.web.6.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/system.text.json/6.0.10/system.text.json.6.0.10.nupkg.sha512",
"/Users/baez/.nuget/packages/system.threading.channels/8.0.0/system.threading.channels.8.0.0.nupkg.sha512",
"/Users/baez/.nuget/packages/tunit/0.6.123/tunit.0.6.123.nupkg.sha512",
"/Users/baez/.nuget/packages/tunit.assertions/0.6.123/tunit.assertions.0.6.123.nupkg.sha512",
"/Users/baez/.nuget/packages/tunit.core/0.6.123/tunit.core.0.6.123.nupkg.sha512",
"/Users/baez/.nuget/packages/tunit.engine/0.6.123/tunit.engine.0.6.123.nupkg.sha512",
"/Users/baez/.nuget/packages/websocket.client/5.1.1/websocket.client.5.1.1.nupkg.sha512"
],
"logs": []
}

View file

@ -0,0 +1 @@
"restore":{"projectUniqueName":"/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/supabase-integration.api-test.csproj","projectName":"supabase-integration.api-test","projectPath":"/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/supabase-integration.api-test.csproj","outputPath":"/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/","projectStyle":"PackageReference","originalTargetFrameworks":["net9.0"],"sources":{"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net9.0":{"targetAlias":"net9.0","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"9.0.100"}"frameworks":{"net9.0":{"targetAlias":"net9.0","dependencies":{"Microsoft.Testing.Extensions.CodeCoverage":{"target":"Package","version":"[17.13.1, )"},"Supabase":{"target":"Package","version":"[1.1.1, )"},"TUnit":{"target":"Package","version":"[0.6.123, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.NETCore.App":{"privateAssets":"all"}},"runtimeIdentifierGraphPath":"/usr/local/share/dotnet/sdk/9.0.102/PortableRuntimeIdentifierGraph.json"}}

View file

@ -0,0 +1 @@
17372014475364470

View file

@ -0,0 +1 @@
17372014475364470

View file

@ -0,0 +1,81 @@
{
"format": 1,
"restore": {
"/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/supabase-integration.api-test.csproj": {}
},
"projects": {
"/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/supabase-integration.api-test.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/supabase-integration.api-test.csproj",
"projectName": "supabase-integration.api-test",
"projectPath": "/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/supabase-integration.api-test.csproj",
"packagesPath": "/Users/baez/.nuget/packages/",
"outputPath": "/Users/baez/sources/code.icb4dc0.de/prskr/supabase-operator/testdata/dotnet-client/test/supabase-integration.api-test/obj/",
"projectStyle": "PackageReference",
"configFilePaths": [
"/Users/baez/.nuget/NuGet/NuGet.Config"
],
"originalTargetFrameworks": [
"net9.0"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "9.0.100"
},
"frameworks": {
"net9.0": {
"targetAlias": "net9.0",
"dependencies": {
"Microsoft.Testing.Extensions.CodeCoverage": {
"target": "Package",
"version": "[17.13.1, )"
},
"Supabase": {
"target": "Package",
"version": "[1.1.1, )"
},
"TUnit": {
"target": "Package",
"version": "[0.6.123, )"
}
},
"imports": [
"net461",
"net462",
"net47",
"net471",
"net472",
"net48",
"net481"
],
"assetTargetFallback": true,
"warn": true,
"frameworkReferences": {
"Microsoft.NETCore.App": {
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "/usr/local/share/dotnet/sdk/9.0.102/PortableRuntimeIdentifierGraph.json"
}
}
}
}
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/Users/baez/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/Users/baez/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.12.2</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="/Users/baez/.nuget/packages/" />
</ItemGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)tunit.core/0.6.123/buildTransitive/net9.0/TUnit.Core.props" Condition="Exists('$(NuGetPackageRoot)tunit.core/0.6.123/buildTransitive/net9.0/TUnit.Core.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.testing.platform/1.4.3/buildTransitive/net8.0/Microsoft.Testing.Platform.props" Condition="Exists('$(NuGetPackageRoot)microsoft.testing.platform/1.4.3/buildTransitive/net8.0/Microsoft.Testing.Platform.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.testing.platform.msbuild/1.4.3/buildTransitive/net8.0/Microsoft.Testing.Platform.MSBuild.props" Condition="Exists('$(NuGetPackageRoot)microsoft.testing.platform.msbuild/1.4.3/buildTransitive/net8.0/Microsoft.Testing.Platform.MSBuild.props')" />
<Import Project="$(NuGetPackageRoot)tunit.engine/0.6.123/buildTransitive/net9.0/TUnit.Engine.props" Condition="Exists('$(NuGetPackageRoot)tunit.engine/0.6.123/buildTransitive/net9.0/TUnit.Engine.props')" />
<Import Project="$(NuGetPackageRoot)tunit.assertions/0.6.123/buildTransitive/net9.0/TUnit.Assertions.props" Condition="Exists('$(NuGetPackageRoot)tunit.assertions/0.6.123/buildTransitive/net9.0/TUnit.Assertions.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.testing.extensions.codecoverage/17.13.1/buildTransitive/net6.0/Microsoft.Testing.Extensions.CodeCoverage.props" Condition="Exists('$(NuGetPackageRoot)microsoft.testing.extensions.codecoverage/17.13.1/buildTransitive/net6.0/Microsoft.Testing.Extensions.CodeCoverage.props')" />
</ImportGroup>
</Project>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/8.0.0/buildTransitive/net6.0/Microsoft.Extensions.Logging.Abstractions.targets')" />
<Import Project="$(NuGetPackageRoot)tunit.core/0.6.123/buildTransitive/net9.0/TUnit.Core.targets" Condition="Exists('$(NuGetPackageRoot)tunit.core/0.6.123/buildTransitive/net9.0/TUnit.Core.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.testing.platform.msbuild/1.4.3/buildTransitive/net8.0/Microsoft.Testing.Platform.MSBuild.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.testing.platform.msbuild/1.4.3/buildTransitive/net8.0/Microsoft.Testing.Platform.MSBuild.targets')" />
<Import Project="$(NuGetPackageRoot)tunit.assertions/0.6.123/buildTransitive/net9.0/TUnit.Assertions.targets" Condition="Exists('$(NuGetPackageRoot)tunit.assertions/0.6.123/buildTransitive/net9.0/TUnit.Assertions.targets')" />
<Import Project="$(NuGetPackageRoot)system.text.json/6.0.10/buildTransitive/netcoreapp3.1/System.Text.Json.targets" Condition="Exists('$(NuGetPackageRoot)system.text.json/6.0.10/buildTransitive/netcoreapp3.1/System.Text.Json.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.testing.extensions.codecoverage/17.13.1/buildTransitive/net6.0/Microsoft.Testing.Extensions.CodeCoverage.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.testing.extensions.codecoverage/17.13.1/buildTransitive/net6.0/Microsoft.Testing.Extensions.CodeCoverage.targets')" />
</ImportGroup>
</Project>

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType>
<RootNamespace>supabase_integration.api_test</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Supabase" Version="1.1.1" />
<PackageReference Include="TUnit" Version="0.6.123" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" Version="17.13.1" />
</ItemGroup>
<ItemGroup>
<Using Include="TUnit.Core" />
</ItemGroup>
</Project>