package controlplane import ( "context" "slices" corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" router "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" "github.com/envoyproxy/go-control-plane/pkg/cache/types" "github.com/envoyproxy/go-control-plane/pkg/cache/v3" "github.com/envoyproxy/go-control-plane/pkg/resource/v3" "google.golang.org/protobuf/types/known/anypb" discoveryv1 "k8s.io/api/discovery/v1" "sigs.k8s.io/controller-runtime/pkg/log" "code.icb4dc0.de/prskr/supabase-operator/internal/supabase" ) 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 (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) } 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 EnvoyServices) Targets() map[string][]string { targets := make(map[string][]string) if s.Postgrest != nil { targets[supabase.ServiceConfig.Postgrest.Name] = s.Postgrest.Targets() } if s.GoTrue != nil { targets[supabase.ServiceConfig.Auth.Name] = s.GoTrue.Targets() } if s.PGMeta != nil { targets[supabase.ServiceConfig.PGMeta.Name] = s.PGMeta.Targets() } if s.Studio != nil { targets[supabase.ServiceConfig.Studio.Name] = s.Studio.Targets() } return targets } func (s *EnvoyServices) snapshot(ctx context.Context, instance, version string) (*cache.Snapshot, error) { const ( apiRouteName = "supabase" studioRouteName = "supabas-studio" vHostName = "supabase" listenerName = "supabase" ) logger := log.FromContext(ctx) apiConnectionManager := &hcm.HttpConnectionManager{ CodecType: hcm.HttpConnectionManager_AUTO, StatPrefix: "http", RouteSpecifier: &hcm.HttpConnectionManager_Rds{ Rds: &hcm.Rds{ ConfigSource: &corev3.ConfigSource{ ResourceApiVersion: resource.DefaultAPIVersion, ConfigSourceSpecifier: &corev3.ConfigSource_ApiConfigSource{ ApiConfigSource: &corev3.ApiConfigSource{ TransportApiVersion: resource.DefaultAPIVersion, ApiType: corev3.ApiConfigSource_GRPC, SetNodeOnFirstMessageOnly: true, GrpcServices: []*corev3.GrpcService{{ TargetSpecifier: &corev3.GrpcService_EnvoyGrpc_{ EnvoyGrpc: &corev3.GrpcService_EnvoyGrpc{ClusterName: "supabase-control-plane"}, }, }}, }, }, }, RouteConfigName: apiRouteName, }, }, HttpFilters: []*hcm.HttpFilter{ { Name: FilterNameJwtAuthn, ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(JWTFilterConfig())}, }, { Name: FilterNameCORS, ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(Cors())}, }, { Name: FilterNameHttpRouter, ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(new(router.Router))}, }, }, } studioConnetionManager := &hcm.HttpConnectionManager{ CodecType: hcm.HttpConnectionManager_AUTO, StatPrefix: "http", RouteSpecifier: &hcm.HttpConnectionManager_Rds{ Rds: &hcm.Rds{ ConfigSource: &corev3.ConfigSource{ ResourceApiVersion: resource.DefaultAPIVersion, ConfigSourceSpecifier: &corev3.ConfigSource_ApiConfigSource{ ApiConfigSource: &corev3.ApiConfigSource{ TransportApiVersion: resource.DefaultAPIVersion, ApiType: corev3.ApiConfigSource_GRPC, SetNodeOnFirstMessageOnly: true, GrpcServices: []*corev3.GrpcService{{ TargetSpecifier: &corev3.GrpcService_EnvoyGrpc_{ EnvoyGrpc: &corev3.GrpcService_EnvoyGrpc{ClusterName: "supabase-control-plane"}, }, }}, }, }, }, RouteConfigName: studioRouteName, }, }, HttpFilters: []*hcm.HttpFilter{ { Name: FilterNameHttpRouter, ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(new(router.Router))}, }, }, } apiRouteCfg := &route.RouteConfiguration{ Name: apiRouteName, VirtualHosts: []*route.VirtualHost{{ Name: "supabase", Domains: []string{"*"}, TypedPerFilterConfig: map[string]*anypb.Any{ FilterNameJwtAuthn: MustAny(JWTPerRouteConfig()), FilterNameRBAC: MustAny(RBACPerRoute(RBACRequireAuthConfig())), }, Routes: slices.Concat( s.Postgrest.Routes(instance), s.GoTrue.Routes(instance), s.PGMeta.Routes(instance), ), }}, TypedPerFilterConfig: map[string]*anypb.Any{ FilterNameCORS: MustAny(CorsPolicy()), }, } // TODO add studio route config listeners := []*listenerv3.Listener{{ Name: listenerName, Address: &corev3.Address{ Address: &corev3.Address_SocketAddress{ SocketAddress: &corev3.SocketAddress{ Protocol: corev3.SocketAddress_TCP, Address: "0.0.0.0", PortSpecifier: &corev3.SocketAddress_PortValue{ PortValue: 8000, }, }, }, }, FilterChains: []*listenerv3.FilterChain{ { Filters: []*listenerv3.Filter{ { Name: FilterNameHttpConnectionManager, ConfigType: &listenerv3.Filter_TypedConfig{ TypedConfig: MustAny(apiConnectionManager), }, }, }, }, }, }} if s.Studio != nil { logger.Info("Adding studio listener") listeners = append(listeners, &listenerv3.Listener{ Name: "studio", Address: &corev3.Address{ Address: &corev3.Address_SocketAddress{ SocketAddress: &corev3.SocketAddress{ Protocol: corev3.SocketAddress_TCP, Address: "0.0.0.0", PortSpecifier: &corev3.SocketAddress_PortValue{ PortValue: 3000, }, }, }, }, FilterChains: []*listenerv3.FilterChain{ { Filters: []*listenerv3.Filter{ { Name: FilterNameHttpConnectionManager, ConfigType: &listenerv3.Filter_TypedConfig{ TypedConfig: MustAny(studioConnetionManager), }, }, }, }, }, }) } rawSnapshot := map[resource.Type][]types.Resource{ resource.ClusterType: castResources( slices.Concat( s.Postgrest.Cluster(instance), s.GoTrue.Cluster(instance), s.PGMeta.Cluster(instance), )...), resource.RouteType: {apiRouteCfg}, resource.ListenerType: castResources(listeners...), } snapshot, err := cache.NewSnapshot( version, rawSnapshot, ) if err != nil { return nil, err } if err := snapshot.Consistent(); err != nil { return nil, err } return snapshot, nil } func castResources[T types.Resource](from ...T) []types.Resource { result := make([]types.Resource, len(from)) for idx := range from { result[idx] = from[idx] } return result }