supabase-operator/internal/controlplane/snapshot.go

268 lines
7.3 KiB
Go
Raw Normal View History

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
}