feat(dashboard): PoC Oauth2 auth
This commit is contained in:
parent
89b682935b
commit
0fccef973f
53 changed files with 1914 additions and 331 deletions
Tiltfile
api/v1alpha1
cmd
config
control-plane
crd/bases
default
dev
.gitignoreapigateway.yamlcnpg-cluster.yamlcore.yamldashboard.yamlkustomization.yamlminio-operator.yamlminio.yamlnamespace.yamlstorage.yamlstudio-credentials-secret.yaml
manager
rbac
samples
dev
go.modgo.suminternal
certs
controller
apigateway_controller.gocore_gotrue_controller.gocore_postgrest_controller.godashboard_pg-meta_controller.godashboard_studio_controller.gostorage_api_controller.gostorage_imgproxy_controller.go
templates
controlplane
health
supabase
4
Tiltfile
4
Tiltfile
|
@ -61,7 +61,7 @@ k8s_resource(
|
||||||
k8s_resource(
|
k8s_resource(
|
||||||
objects=["gateway-sample:APIGateway:supabase-demo"],
|
objects=["gateway-sample:APIGateway:supabase-demo"],
|
||||||
extra_pod_selectors={"app.kubernetes.io/component": "api-gateway"},
|
extra_pod_selectors={"app.kubernetes.io/component": "api-gateway"},
|
||||||
port_forwards=[8000, 19000],
|
port_forwards=[3000, 8000, 19000],
|
||||||
new_name='API Gateway',
|
new_name='API Gateway',
|
||||||
resource_deps=[
|
resource_deps=[
|
||||||
'supabase-controller-manager'
|
'supabase-controller-manager'
|
||||||
|
@ -72,7 +72,7 @@ k8s_resource(
|
||||||
objects=["dashboard-sample:Dashboard:supabase-demo"],
|
objects=["dashboard-sample:Dashboard:supabase-demo"],
|
||||||
extra_pod_selectors={"app.kubernetes.io/component": "dashboard", "app.kubernetes.io/name": "studio"},
|
extra_pod_selectors={"app.kubernetes.io/component": "dashboard", "app.kubernetes.io/name": "studio"},
|
||||||
discovery_strategy="selectors-only",
|
discovery_strategy="selectors-only",
|
||||||
port_forwards=[3000],
|
# port_forwards=[3000],
|
||||||
new_name='Dashboard',
|
new_name='Dashboard',
|
||||||
resource_deps=[
|
resource_deps=[
|
||||||
'supabase-controller-manager'
|
'supabase-controller-manager'
|
||||||
|
|
|
@ -46,14 +46,111 @@ type EnvoySpec struct {
|
||||||
ControlPlane *ControlPlaneSpec `json:"controlPlane"`
|
ControlPlane *ControlPlaneSpec `json:"controlPlane"`
|
||||||
// WorkloadTemplate - customize the Envoy deployment
|
// WorkloadTemplate - customize the Envoy deployment
|
||||||
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
|
WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"`
|
||||||
|
// DisableIPv6 - disable IPv6 for the Envoy instance
|
||||||
|
// this will force Envoy to use IPv4 for upstream hosts (mostly for the OAuth2 token endpoint)
|
||||||
|
DisableIPv6 bool `json:"disableIPv6,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TlsCertRef struct {
|
||||||
|
SecretName string `json:"secretName"`
|
||||||
|
// ServerCertKey - key in the secret that contains the server certificate
|
||||||
|
// +kubebuilder:default="tls.crt"
|
||||||
|
ServerCertKey string `json:"serverCertKey"`
|
||||||
|
// ServerKeyKey - key in the secret that contains the server private key
|
||||||
|
// +kubebuilder:default="tls.key"
|
||||||
|
ServerKeyKey string `json:"serverKeyKey"`
|
||||||
|
// CaCertKey - key in the secret that contains the CA certificate
|
||||||
|
// +kubebuilder:default="ca.crt"
|
||||||
|
CaCertKey string `json:"caCertKey,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndpointTlsSpec struct {
|
||||||
|
Cert *TlsCertRef `json:"cert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiEndpointSpec struct {
|
type ApiEndpointSpec struct {
|
||||||
// JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs
|
// JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs
|
||||||
JWKSSelector *corev1.SecretKeySelector `json:"jwks"`
|
JWKSSelector *corev1.SecretKeySelector `json:"jwks"`
|
||||||
|
// TLS - enable and configure TLS for the API endpoint
|
||||||
|
TLS *EndpointTlsSpec `json:"tls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardEndpointSpec struct{}
|
func (s *ApiEndpointSpec) TLSSpec() *EndpointTlsSpec {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.TLS
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardAuthType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DashboardAuthTypeNone DashboardAuthType = "none"
|
||||||
|
DashboardAuthTypeOAuth2 DashboardAuthType = "oauth2"
|
||||||
|
DashboardAuthTypeBasic DashboardAuthType = "basic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DashboardOAuth2Spec struct {
|
||||||
|
// TokenEndpoint - endpoint where Envoy will retrieve the OAuth2 access and identity token from
|
||||||
|
TokenEndpoint string `json:"tokenEndpoint"`
|
||||||
|
// AuthorizationEndpoint - endpoint where the user will be redirected to authenticate
|
||||||
|
AuthorizationEndpoint string `json:"authorizationEndpoint"`
|
||||||
|
// ClientID - client ID to authenticate with the OAuth2 provider
|
||||||
|
ClientID string `json:"clientId"`
|
||||||
|
// Scopes - scopes to request from the OAuth2 provider (e.g. "openid", "profile", ...) - optional
|
||||||
|
Scopes []string `json:"scopes,omitempty"`
|
||||||
|
// Resources - resources to request from the OAuth2 provider (e.g. "user", "email", ...) - optional
|
||||||
|
Resources []string `json:"resources,omitempty"`
|
||||||
|
// ClientSecretRef - reference to the secret that contains the client secret
|
||||||
|
ClientSecretRef *corev1.SecretKeySelector `json:"clientSecretRef"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardBasicAuthSpec struct{}
|
||||||
|
|
||||||
|
type DashboardAuthSpec struct {
|
||||||
|
OAuth2 *DashboardOAuth2Spec `json:"oauth2,omitempty"`
|
||||||
|
Basic *DashboardBasicAuthSpec `json:"basic,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardEndpointSpec struct {
|
||||||
|
// Auth - configure authentication for the dashboard endpoint
|
||||||
|
Auth *DashboardAuthSpec `json:"auth,omitempty"`
|
||||||
|
// TLS - enable and configure TLS for the Dashboard endpoint
|
||||||
|
TLS *EndpointTlsSpec `json:"tls,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DashboardEndpointSpec) TLSSpec() *EndpointTlsSpec {
|
||||||
|
if s == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.TLS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DashboardEndpointSpec) AuthType() DashboardAuthType {
|
||||||
|
if s == nil || s.Auth == nil {
|
||||||
|
return DashboardAuthTypeNone
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Auth.OAuth2 != nil {
|
||||||
|
return DashboardAuthTypeOAuth2
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Auth.Basic != nil {
|
||||||
|
return DashboardAuthTypeBasic
|
||||||
|
}
|
||||||
|
|
||||||
|
return DashboardAuthTypeNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DashboardEndpointSpec) OAuth2() *DashboardOAuth2Spec {
|
||||||
|
if s == nil || s.Auth == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Auth.OAuth2
|
||||||
|
}
|
||||||
|
|
||||||
// APIGatewaySpec defines the desired state of APIGateway.
|
// APIGatewaySpec defines the desired state of APIGateway.
|
||||||
type APIGatewaySpec struct {
|
type APIGatewaySpec struct {
|
||||||
|
|
|
@ -21,7 +21,7 @@ limitations under the License.
|
||||||
package v1alpha1
|
package v1alpha1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
v1 "k8s.io/api/core/v1"
|
"k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
@ -101,7 +101,7 @@ func (in *APIGatewaySpec) DeepCopyInto(out *APIGatewaySpec) {
|
||||||
if in.DashboardEndpoint != nil {
|
if in.DashboardEndpoint != nil {
|
||||||
in, out := &in.DashboardEndpoint, &out.DashboardEndpoint
|
in, out := &in.DashboardEndpoint, &out.DashboardEndpoint
|
||||||
*out = new(DashboardEndpointSpec)
|
*out = new(DashboardEndpointSpec)
|
||||||
**out = **in
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
if in.ServiceSelector != nil {
|
if in.ServiceSelector != nil {
|
||||||
in, out := &in.ServiceSelector, &out.ServiceSelector
|
in, out := &in.ServiceSelector, &out.ServiceSelector
|
||||||
|
@ -160,6 +160,11 @@ func (in *ApiEndpointSpec) DeepCopyInto(out *ApiEndpointSpec) {
|
||||||
*out = new(v1.SecretKeySelector)
|
*out = new(v1.SecretKeySelector)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
|
if in.TLS != nil {
|
||||||
|
in, out := &in.TLS, &out.TLS
|
||||||
|
*out = new(EndpointTlsSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiEndpointSpec.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiEndpointSpec.
|
||||||
|
@ -490,6 +495,46 @@ func (in *Dashboard) DeepCopyObject() runtime.Object {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *DashboardAuthSpec) DeepCopyInto(out *DashboardAuthSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.OAuth2 != nil {
|
||||||
|
in, out := &in.OAuth2, &out.OAuth2
|
||||||
|
*out = new(DashboardOAuth2Spec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.Basic != nil {
|
||||||
|
in, out := &in.Basic, &out.Basic
|
||||||
|
*out = new(DashboardBasicAuthSpec)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardAuthSpec.
|
||||||
|
func (in *DashboardAuthSpec) DeepCopy() *DashboardAuthSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DashboardAuthSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *DashboardBasicAuthSpec) DeepCopyInto(out *DashboardBasicAuthSpec) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardBasicAuthSpec.
|
||||||
|
func (in *DashboardBasicAuthSpec) DeepCopy() *DashboardBasicAuthSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DashboardBasicAuthSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *DashboardDbSpec) DeepCopyInto(out *DashboardDbSpec) {
|
func (in *DashboardDbSpec) DeepCopyInto(out *DashboardDbSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -513,6 +558,16 @@ func (in *DashboardDbSpec) DeepCopy() *DashboardDbSpec {
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *DashboardEndpointSpec) DeepCopyInto(out *DashboardEndpointSpec) {
|
func (in *DashboardEndpointSpec) DeepCopyInto(out *DashboardEndpointSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
if in.Auth != nil {
|
||||||
|
in, out := &in.Auth, &out.Auth
|
||||||
|
*out = new(DashboardAuthSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
if in.TLS != nil {
|
||||||
|
in, out := &in.TLS, &out.TLS
|
||||||
|
*out = new(EndpointTlsSpec)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardEndpointSpec.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardEndpointSpec.
|
||||||
|
@ -557,6 +612,36 @@ func (in *DashboardList) DeepCopyObject() runtime.Object {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *DashboardOAuth2Spec) DeepCopyInto(out *DashboardOAuth2Spec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Scopes != nil {
|
||||||
|
in, out := &in.Scopes, &out.Scopes
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.Resources != nil {
|
||||||
|
in, out := &in.Resources, &out.Resources
|
||||||
|
*out = make([]string, len(*in))
|
||||||
|
copy(*out, *in)
|
||||||
|
}
|
||||||
|
if in.ClientSecretRef != nil {
|
||||||
|
in, out := &in.ClientSecretRef, &out.ClientSecretRef
|
||||||
|
*out = new(v1.SecretKeySelector)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardOAuth2Spec.
|
||||||
|
func (in *DashboardOAuth2Spec) DeepCopy() *DashboardOAuth2Spec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(DashboardOAuth2Spec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *DashboardSpec) DeepCopyInto(out *DashboardSpec) {
|
func (in *DashboardSpec) DeepCopyInto(out *DashboardSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -768,6 +853,26 @@ func (in *EmailAuthSmtpSpec) DeepCopy() *EmailAuthSmtpSpec {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *EndpointTlsSpec) DeepCopyInto(out *EndpointTlsSpec) {
|
||||||
|
*out = *in
|
||||||
|
if in.Cert != nil {
|
||||||
|
in, out := &in.Cert, &out.Cert
|
||||||
|
*out = new(TlsCertRef)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointTlsSpec.
|
||||||
|
func (in *EndpointTlsSpec) DeepCopy() *EndpointTlsSpec {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(EndpointTlsSpec)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *EnvoySpec) DeepCopyInto(out *EnvoySpec) {
|
func (in *EnvoySpec) DeepCopyInto(out *EnvoySpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
@ -1261,6 +1366,21 @@ func (in *StudioSpec) DeepCopy() *StudioSpec {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *TlsCertRef) DeepCopyInto(out *TlsCertRef) {
|
||||||
|
*out = *in
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TlsCertRef.
|
||||||
|
func (in *TlsCertRef) DeepCopy() *TlsCertRef {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(TlsCertRef)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
func (in *UploadTempSpec) DeepCopyInto(out *UploadTempSpec) {
|
func (in *UploadTempSpec) DeepCopyInto(out *UploadTempSpec) {
|
||||||
*out = *in
|
*out = *in
|
||||||
|
|
|
@ -19,8 +19,10 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
clusterservice "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
|
clusterservice "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
|
||||||
|
@ -32,30 +34,52 @@ import (
|
||||||
secretservice "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3"
|
secretservice "github.com/envoyproxy/go-control-plane/envoy/service/secret/v3"
|
||||||
cachev3 "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"
|
"github.com/envoyproxy/go-control-plane/pkg/server/v3"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
grpchealth "google.golang.org/grpc/health"
|
grpchealth "google.golang.org/grpc/health"
|
||||||
"google.golang.org/grpc/health/grpc_health_v1"
|
"google.golang.org/grpc/health/grpc_health_v1"
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
"google.golang.org/grpc/reflection"
|
"google.golang.org/grpc/reflection"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
mgr "sigs.k8s.io/controller-runtime/pkg/manager"
|
mgr "sigs.k8s.io/controller-runtime/pkg/manager"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/certs"
|
||||||
"code.icb4dc0.de/prskr/supabase-operator/internal/controlplane"
|
"code.icb4dc0.de/prskr/supabase-operator/internal/controlplane"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/health"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:lll // flag declaration with struct tags is as long as it is
|
//nolint:lll // flag declaration with struct tags is as long as it is
|
||||||
type controlPlane struct {
|
type controlPlane struct {
|
||||||
|
caCert tls.Certificate `kong:"-"`
|
||||||
|
|
||||||
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."`
|
||||||
|
Tls struct {
|
||||||
|
CA struct {
|
||||||
|
Cert FileContent `env:"CERT" name:"server-cert" required:"" help:"The path to the server certificate file."`
|
||||||
|
Key FileContent `env:"KEY" name:"server-key" required:"" help:"The path to the server key file."`
|
||||||
|
} `embed:"" prefix:"ca." envprefix:"CA_"`
|
||||||
|
ServerSecretName string `name:"server-secret-name" help:"The name of the secret containing the server certificate and key." default:"control-plane-xds-tls"`
|
||||||
|
} `embed:"" prefix:"tls." envprefix:"TLS_"`
|
||||||
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."`
|
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."`
|
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."`
|
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."`
|
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"`
|
EnableHTTP2 bool `name:"enable-http2" default:"false" help:"If set, HTTP/2 will be enabled for the metrics and webhook servers"`
|
||||||
|
ServiceName string `name:"service-name" env:"CONTROL_PLANE_SERVICE_NAME" default:"" required:"" help:"The name of the control plane service."`
|
||||||
|
Namespace string `name:"namespace" env:"CONTROL_PLANE_NAMESPACE" default:"" required:"" help:"Namespace where the controller is running, ideally set via downward API"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp controlPlane) Run(ctx context.Context) error {
|
func (cp *controlPlane) Run(ctx context.Context) error {
|
||||||
var tlsOpts []func(*tls.Config)
|
var tlsOpts []func(*tls.Config)
|
||||||
|
|
||||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||||
|
@ -91,6 +115,11 @@ func (cp controlPlane) Run(ctx context.Context) error {
|
||||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bootstrapClient, err := client.New(ctrl.GetConfigOrDie(), client.Options{Scheme: scheme})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create bootstrap client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
Scheme: scheme,
|
Scheme: scheme,
|
||||||
Metrics: metricsServerOptions,
|
Metrics: metricsServerOptions,
|
||||||
|
@ -106,7 +135,12 @@ func (cp controlPlane) Run(ctx context.Context) error {
|
||||||
|
|
||||||
envoySnapshotCache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, nil)
|
envoySnapshotCache := cachev3.NewSnapshotCache(false, cachev3.IDHash{}, nil)
|
||||||
|
|
||||||
envoySrv, err := cp.envoyServer(ctx, envoySnapshotCache)
|
serverCert, err := cp.ensureControlPlaneTlsCert(ctx, bootstrapClient)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure control plane TLS cert: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envoySrv, err := cp.envoyServer(ctx, envoySnapshotCache, serverCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -123,6 +157,18 @@ func (cp controlPlane) Run(ctx context.Context) error {
|
||||||
return fmt.Errorf("unable to create controller Core DB: %w", err)
|
return fmt.Errorf("unable to create controller Core DB: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||||
|
return fmt.Errorf("unable to set up health check: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.AddHealthzCheck("server-cert", health.CertValidCheck(serverCert)); err != nil {
|
||||||
|
return fmt.Errorf("unable to set up health check: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||||
|
return fmt.Errorf("unable to set up ready check: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
setupLog.Info("starting manager")
|
setupLog.Info("starting manager")
|
||||||
if err := mgr.Start(ctx); err != nil {
|
if err := mgr.Start(ctx); err != nil {
|
||||||
return fmt.Errorf("problem running manager: %w", err)
|
return fmt.Errorf("problem running manager: %w", err)
|
||||||
|
@ -131,9 +177,19 @@ func (cp controlPlane) Run(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp controlPlane) envoyServer(
|
func (cp *controlPlane) AfterApply() (err error) {
|
||||||
|
cp.caCert, err = tls.X509KeyPair(cp.Tls.CA.Cert, cp.Tls.CA.Key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse server certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *controlPlane) envoyServer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cache cachev3.SnapshotCache,
|
cache cachev3.SnapshotCache,
|
||||||
|
serverCert tls.Certificate,
|
||||||
) (runnable mgr.Runnable, err error) {
|
) (runnable mgr.Runnable, err error) {
|
||||||
const (
|
const (
|
||||||
grpcKeepaliveTime = 30 * time.Second
|
grpcKeepaliveTime = 30 * time.Second
|
||||||
|
@ -153,7 +209,17 @@ func (cp controlPlane) envoyServer(
|
||||||
// availability problems. Keepalive timeouts based on connection_keepalive parameter
|
// availability problems. Keepalive timeouts based on connection_keepalive parameter
|
||||||
// https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/examples#dynamic
|
// https://www.envoyproxy.io/docs/envoy/latest/configuration/overview/examples#dynamic
|
||||||
|
|
||||||
|
tlsCfg, err := cp.tlsConfig(serverCert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create TLS config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loggingOpts := []logging.Option{
|
||||||
|
logging.WithLogOnEvents(logging.StartCall, logging.FinishCall),
|
||||||
|
}
|
||||||
|
|
||||||
grpcOptions := append(make([]grpc.ServerOption, 0, 4),
|
grpcOptions := append(make([]grpc.ServerOption, 0, 4),
|
||||||
|
grpc.Creds(credentials.NewTLS(tlsCfg)),
|
||||||
grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams),
|
grpc.MaxConcurrentStreams(grpcMaxConcurrentStreams),
|
||||||
grpc.KeepaliveParams(keepalive.ServerParameters{
|
grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||||
Time: grpcKeepaliveTime,
|
Time: grpcKeepaliveTime,
|
||||||
|
@ -163,6 +229,12 @@ func (cp controlPlane) envoyServer(
|
||||||
MinTime: grpcKeepaliveMinTime,
|
MinTime: grpcKeepaliveMinTime,
|
||||||
PermitWithoutStream: true,
|
PermitWithoutStream: true,
|
||||||
}),
|
}),
|
||||||
|
grpc.ChainUnaryInterceptor(
|
||||||
|
logging.UnaryServerInterceptor(InterceptorLogger(ctrl.Log), loggingOpts...),
|
||||||
|
),
|
||||||
|
grpc.ChainStreamInterceptor(
|
||||||
|
logging.StreamServerInterceptor(InterceptorLogger(ctrl.Log), loggingOpts...),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
grpcServer := grpc.NewServer(grpcOptions...)
|
grpcServer := grpc.NewServer(grpcOptions...)
|
||||||
|
|
||||||
|
@ -195,3 +267,90 @@ func (cp controlPlane) envoyServer(
|
||||||
return grpcServer.Serve(lis)
|
return grpcServer.Serve(lis)
|
||||||
}), nil
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cp *controlPlane) ensureControlPlaneTlsCert(ctx context.Context, k8sClient client.Client) (tls.Certificate, error) {
|
||||||
|
var (
|
||||||
|
controlPlaneServerCert = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: cp.Tls.ServerSecretName,
|
||||||
|
Namespace: cp.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serverCert tls.Certificate
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, k8sClient, controlPlaneServerCert, func() (err error) {
|
||||||
|
controlPlaneServerCert.Type = corev1.SecretTypeTLS
|
||||||
|
|
||||||
|
if controlPlaneServerCert.Data == nil {
|
||||||
|
controlPlaneServerCert.Data = make(map[string][]byte, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cert = controlPlaneServerCert.Data[corev1.TLSCertKey]
|
||||||
|
privateKey = controlPlaneServerCert.Data[corev1.TLSPrivateKeyKey]
|
||||||
|
)
|
||||||
|
|
||||||
|
var requireRenewal bool
|
||||||
|
if cert != nil && privateKey != nil {
|
||||||
|
if serverCert, err = tls.X509KeyPair(cert, privateKey); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse server certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
renewGracePeriod := time.Duration(float64(serverCert.Leaf.NotAfter.Sub(serverCert.Leaf.NotBefore)) * 0.1)
|
||||||
|
if serverCert.Leaf.NotAfter.Before(time.Now().Add(-renewGracePeriod)) {
|
||||||
|
requireRenewal = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requireRenewal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if requireRenewal {
|
||||||
|
dnsNames := []string{
|
||||||
|
strings.Join([]string{cp.ServiceName, cp.Namespace, "svc"}, "."),
|
||||||
|
strings.Join([]string{cp.ServiceName, cp.Namespace, "svc", "cluster", "local"}, "."),
|
||||||
|
}
|
||||||
|
if certResult, err := certs.ServerCert("supabase-control-plane", dnsNames, cp.caCert); err != nil {
|
||||||
|
return fmt.Errorf("failed to generate server certificate: %w", err)
|
||||||
|
} else {
|
||||||
|
serverCert = certResult.ServerCert
|
||||||
|
controlPlaneServerCert.Data[corev1.TLSCertKey] = certResult.PublicKey
|
||||||
|
controlPlaneServerCert.Data[corev1.TLSPrivateKeyKey] = certResult.PrivateKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return tls.Certificate{}, fmt.Errorf("failed to create or update control plane server certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverCert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *controlPlane) tlsConfig(serverCert tls.Certificate) (*tls.Config, error) {
|
||||||
|
tlsCfg := &tls.Config{
|
||||||
|
RootCAs: x509.NewCertPool(),
|
||||||
|
ClientCAs: x509.NewCertPool(),
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCfg.Certificates = append(tlsCfg.Certificates, serverCert)
|
||||||
|
if !tlsCfg.RootCAs.AppendCertsFromPEM(cp.Tls.CA.Cert) {
|
||||||
|
return nil, fmt.Errorf("failed to parse CA certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tlsCfg.ClientCAs.AppendCertsFromPEM(cp.Tls.CA.Cert) {
|
||||||
|
return nil, fmt.Errorf("failed to parse client CA certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterceptorLogger adapts slog logger to interceptor logger.
|
||||||
|
// This code is simple enough to be copied and not imported.
|
||||||
|
func InterceptorLogger(l logr.Logger) logging.Logger {
|
||||||
|
return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) {
|
||||||
|
l.Info(msg, fields...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
41
cmd/flags.go
Normal file
41
cmd/flags.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ kong.MapperValue = (*FileContent)(nil)
|
||||||
|
|
||||||
|
type FileContent []byte
|
||||||
|
|
||||||
|
func (f *FileContent) Decode(ctx *kong.DecodeContext) (err error) {
|
||||||
|
var filePath string
|
||||||
|
if err := ctx.Scan.PopValueInto("file-content", &filePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if *f, err = os.ReadFile(filePath); err != nil {
|
||||||
|
return fmt.Errorf("failed to read file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -40,6 +40,10 @@ type manager struct {
|
||||||
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."`
|
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"`
|
EnableHTTP2 bool `name:"enable-http2" default:"false" help:"If set, HTTP/2 will be enabled for the metrics and webhook servers"`
|
||||||
Namespace string `name:"controller-namespace" env:"CONTROLLER_NAMESPACE" default:"" help:"Namespace where the controller is running, ideally set via downward API"`
|
Namespace string `name:"controller-namespace" env:"CONTROLLER_NAMESPACE" default:"" help:"Namespace where the controller is running, ideally set via downward API"`
|
||||||
|
Tls struct {
|
||||||
|
CACert FileContent `env:"CA_CERT" name:"ca-cert" required:"" help:"The path to the CA certificate file."`
|
||||||
|
CAKey FileContent `env:"CA_KEY" name:"ca-key" required:"" help:"The path to the CA key file."`
|
||||||
|
} `embed:"" prefix:"tls." envprefix:"TLS_"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m manager) Run(ctx context.Context) error {
|
func (m manager) Run(ctx context.Context) error {
|
||||||
|
@ -68,6 +72,11 @@ func (m manager) Run(ctx context.Context) error {
|
||||||
TLSOpts: tlsOpts,
|
TLSOpts: tlsOpts,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
caCert, err := tls.X509KeyPair(m.Tls.CACert, m.Tls.CAKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to load CA cert: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||||
// More info:
|
// More info:
|
||||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||||
|
@ -145,6 +154,7 @@ func (m manager) Run(ctx context.Context) error {
|
||||||
if err = (&controller.APIGatewayReconciler{
|
if err = (&controller.APIGatewayReconciler{
|
||||||
Client: mgr.GetClient(),
|
Client: mgr.GetClient(),
|
||||||
Scheme: mgr.GetScheme(),
|
Scheme: mgr.GetScheme(),
|
||||||
|
CACert: caCert,
|
||||||
}).SetupWithManager(ctx, mgr); err != nil {
|
}).SetupWithManager(ctx, mgr); err != nil {
|
||||||
return fmt.Errorf("unable to create controller APIGateway: %w", err)
|
return fmt.Errorf("unable to create controller APIGateway: %w", err)
|
||||||
}
|
}
|
||||||
|
|
33
config/control-plane/cert-ca.yaml
Normal file
33
config/control-plane/cert-ca.yaml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Issuer
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: cp-selfsigned-issuer
|
||||||
|
namespace: system
|
||||||
|
spec:
|
||||||
|
selfSigned: {}
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: certificate
|
||||||
|
app.kubernetes.io/instance: serving-cert
|
||||||
|
app.kubernetes.io/component: certificate
|
||||||
|
app.kubernetes.io/created-by: supabase-operator
|
||||||
|
app.kubernetes.io/part-of: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: cp-ca-cert
|
||||||
|
namespace: supabase-system
|
||||||
|
spec:
|
||||||
|
commonName: control-plane-ca
|
||||||
|
privateKey:
|
||||||
|
algorithm: ECDSA
|
||||||
|
issuerRef:
|
||||||
|
kind: Issuer
|
||||||
|
name: selfsigned-issuer
|
||||||
|
secretName: control-plane-ca-cert-tls
|
||||||
|
isCA: true
|
|
@ -18,40 +18,26 @@ spec:
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: control-plane
|
app.kubernetes.io/name: control-plane
|
||||||
spec:
|
spec:
|
||||||
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
|
|
||||||
# according to the platforms which are supported by your solution.
|
|
||||||
# It is considered best practice to support multiple architectures. You can
|
|
||||||
# build your manager image using the makefile target docker-buildx.
|
|
||||||
# affinity:
|
|
||||||
# nodeAffinity:
|
|
||||||
# requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
# nodeSelectorTerms:
|
|
||||||
# - matchExpressions:
|
|
||||||
# - key: kubernetes.io/arch
|
|
||||||
# operator: In
|
|
||||||
# values:
|
|
||||||
# - amd64
|
|
||||||
# - arm64
|
|
||||||
# - ppc64le
|
|
||||||
# - s390x
|
|
||||||
# - key: kubernetes.io/os
|
|
||||||
# operator: In
|
|
||||||
# values:
|
|
||||||
# - linux
|
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
# TODO(user): For common cases that do not require escalating privileges
|
seccompProfile:
|
||||||
# it is recommended to ensure that all your Pods/Containers are restrictive.
|
type: RuntimeDefault
|
||||||
# More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
|
|
||||||
# Please uncomment the following code if your project does NOT have to work on old Kubernetes
|
|
||||||
# versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ).
|
|
||||||
# seccompProfile:
|
|
||||||
# type: RuntimeDefault
|
|
||||||
containers:
|
containers:
|
||||||
- args:
|
- args:
|
||||||
- control-plane
|
- control-plane
|
||||||
image: supabase-operator:latest
|
image: supabase-operator:latest
|
||||||
name: control-plane
|
name: control-plane
|
||||||
|
env:
|
||||||
|
- name: CONTROL_PLANE_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: CONTROL_PLANE_SERVICE_NAME
|
||||||
|
value: control-plane
|
||||||
|
- name: TLS_CA_CERT
|
||||||
|
value: /etc/supabase/control-plane/certs/tls.crt
|
||||||
|
- name: TLS_CA_KEY
|
||||||
|
value: /etc/supabase/control-plane/certs/tls.key
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 18000
|
- containerPort: 18000
|
||||||
name: grpc
|
name: grpc
|
||||||
|
@ -62,17 +48,17 @@ spec:
|
||||||
drop:
|
drop:
|
||||||
- "ALL"
|
- "ALL"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
grpc:
|
httpGet:
|
||||||
port: 18000
|
path: /healthz
|
||||||
|
port: 8081
|
||||||
initialDelaySeconds: 15
|
initialDelaySeconds: 15
|
||||||
periodSeconds: 20
|
periodSeconds: 20
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
grpc:
|
httpGet:
|
||||||
port: 18000
|
path: /readyz
|
||||||
|
port: 8081
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
# TODO(user): Configure the resources accordingly based on the project requirements.
|
|
||||||
# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 150m
|
cpu: 150m
|
||||||
|
@ -80,5 +66,12 @@ spec:
|
||||||
requests:
|
requests:
|
||||||
cpu: 50m
|
cpu: 50m
|
||||||
memory: 64Mi
|
memory: 64Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: tls-certs
|
||||||
|
mountPath: /etc/supabase/control-plane/certs
|
||||||
|
volumes:
|
||||||
|
- name: tls-certs
|
||||||
|
secret:
|
||||||
|
secretName: control-plane-ca-cert-tls
|
||||||
serviceAccountName: control-plane
|
serviceAccountName: control-plane
|
||||||
terminationGracePeriodSeconds: 10
|
terminationGracePeriodSeconds: 10
|
||||||
|
|
|
@ -2,5 +2,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
|
- cert-ca.yaml
|
||||||
- control-plane.yaml
|
- control-plane.yaml
|
||||||
- service.yaml
|
- service.yaml
|
||||||
|
|
||||||
|
configurations:
|
||||||
|
- kustomizeconfig.yaml
|
||||||
|
|
8
config/control-plane/kustomizeconfig.yaml
Normal file
8
config/control-plane/kustomizeconfig.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# This configuration is for teaching kustomize how to update name ref substitution
|
||||||
|
nameReference:
|
||||||
|
- kind: Issuer
|
||||||
|
group: cert-manager.io
|
||||||
|
fieldSpecs:
|
||||||
|
- kind: Certificate
|
||||||
|
group: cert-manager.io
|
||||||
|
path: spec/issuerRef/name
|
|
@ -73,6 +73,36 @@ spec:
|
||||||
- key
|
- key
|
||||||
type: object
|
type: object
|
||||||
x-kubernetes-map-type: atomic
|
x-kubernetes-map-type: atomic
|
||||||
|
tls:
|
||||||
|
description: TLS - enable and configure TLS for the API endpoint
|
||||||
|
properties:
|
||||||
|
cert:
|
||||||
|
properties:
|
||||||
|
caCertKey:
|
||||||
|
default: ca.crt
|
||||||
|
description: CaCertKey - key in the secret that contains
|
||||||
|
the CA certificate
|
||||||
|
type: string
|
||||||
|
secretName:
|
||||||
|
type: string
|
||||||
|
serverCertKey:
|
||||||
|
default: tls.crt
|
||||||
|
description: ServerCertKey - key in the secret that contains
|
||||||
|
the server certificate
|
||||||
|
type: string
|
||||||
|
serverKeyKey:
|
||||||
|
default: tls.key
|
||||||
|
description: ServerKeyKey - key in the secret that contains
|
||||||
|
the server private key
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- secretName
|
||||||
|
- serverCertKey
|
||||||
|
- serverKeyKey
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- cert
|
||||||
|
type: object
|
||||||
required:
|
required:
|
||||||
- jwks
|
- jwks
|
||||||
type: object
|
type: object
|
||||||
|
@ -85,6 +115,102 @@ spec:
|
||||||
description: |-
|
description: |-
|
||||||
DashboardEndpoint - Configure the endpoint for the Supabase dashboard (studio)
|
DashboardEndpoint - Configure the endpoint for the Supabase dashboard (studio)
|
||||||
this includes optional authentication (basic or Oauth2) for the dashboard
|
this includes optional authentication (basic or Oauth2) for the dashboard
|
||||||
|
properties:
|
||||||
|
auth:
|
||||||
|
description: Auth - configure authentication for the dashboard
|
||||||
|
endpoint
|
||||||
|
properties:
|
||||||
|
basic:
|
||||||
|
type: object
|
||||||
|
oauth2:
|
||||||
|
properties:
|
||||||
|
authorizationEndpoint:
|
||||||
|
description: AuthorizationEndpoint - endpoint where the
|
||||||
|
user will be redirected to authenticate
|
||||||
|
type: string
|
||||||
|
clientId:
|
||||||
|
description: ClientID - client ID to authenticate with
|
||||||
|
the OAuth2 provider
|
||||||
|
type: string
|
||||||
|
clientSecretRef:
|
||||||
|
description: ClientSecretRef - reference to the secret
|
||||||
|
that contains the client secret
|
||||||
|
properties:
|
||||||
|
key:
|
||||||
|
description: The key of the secret to select from. Must
|
||||||
|
be a valid secret key.
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
default: ""
|
||||||
|
description: |-
|
||||||
|
Name of the referent.
|
||||||
|
This field is effectively required, but due to backwards compatibility is
|
||||||
|
allowed to be empty. Instances of this type with an empty value here are
|
||||||
|
almost certainly wrong.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||||
|
type: string
|
||||||
|
optional:
|
||||||
|
description: Specify whether the Secret or its key
|
||||||
|
must be defined
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- key
|
||||||
|
type: object
|
||||||
|
x-kubernetes-map-type: atomic
|
||||||
|
resources:
|
||||||
|
description: Resources - resources to request from the
|
||||||
|
OAuth2 provider (e.g. "user", "email", ...) - optional
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
scopes:
|
||||||
|
description: Scopes - scopes to request from the OAuth2
|
||||||
|
provider (e.g. "openid", "profile", ...) - optional
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
tokenEndpoint:
|
||||||
|
description: TokenEndpoint - endpoint where Envoy will
|
||||||
|
retrieve the OAuth2 access and identity token from
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- authorizationEndpoint
|
||||||
|
- clientId
|
||||||
|
- clientSecretRef
|
||||||
|
- tokenEndpoint
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
tls:
|
||||||
|
description: TLS - enable and configure TLS for the Dashboard
|
||||||
|
endpoint
|
||||||
|
properties:
|
||||||
|
cert:
|
||||||
|
properties:
|
||||||
|
caCertKey:
|
||||||
|
default: ca.crt
|
||||||
|
description: CaCertKey - key in the secret that contains
|
||||||
|
the CA certificate
|
||||||
|
type: string
|
||||||
|
secretName:
|
||||||
|
type: string
|
||||||
|
serverCertKey:
|
||||||
|
default: tls.crt
|
||||||
|
description: ServerCertKey - key in the secret that contains
|
||||||
|
the server certificate
|
||||||
|
type: string
|
||||||
|
serverKeyKey:
|
||||||
|
default: tls.key
|
||||||
|
description: ServerKeyKey - key in the secret that contains
|
||||||
|
the server private key
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- secretName
|
||||||
|
- serverCertKey
|
||||||
|
- serverKeyKey
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- cert
|
||||||
|
type: object
|
||||||
type: object
|
type: object
|
||||||
envoy:
|
envoy:
|
||||||
description: Envoy - configure the envoy instance and most importantly
|
description: Envoy - configure the envoy instance and most importantly
|
||||||
|
@ -108,6 +234,11 @@ spec:
|
||||||
- host
|
- host
|
||||||
- port
|
- port
|
||||||
type: object
|
type: object
|
||||||
|
disableIPv6:
|
||||||
|
description: |-
|
||||||
|
DisableIPv6 - disable IPv6 for the Envoy instance
|
||||||
|
this will force Envoy to use IPv4 for upstream hosts (mostly for the OAuth2 token endpoint)
|
||||||
|
type: boolean
|
||||||
nodeName:
|
nodeName:
|
||||||
description: |-
|
description: |-
|
||||||
NodeName - identifies the Envoy cluster within the current namespace
|
NodeName - identifies the Envoy cluster within the current namespace
|
||||||
|
|
|
@ -23,7 +23,6 @@ resources:
|
||||||
- ../certmanager
|
- ../certmanager
|
||||||
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
|
||||||
#- ../prometheus
|
#- ../prometheus
|
||||||
# [METRICS] Expose the controller manager metrics service.
|
|
||||||
- metrics_service.yaml
|
- metrics_service.yaml
|
||||||
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
|
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
|
||||||
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
|
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
|
||||||
|
@ -44,19 +43,31 @@ patches:
|
||||||
# crd/kustomization.yaml
|
# crd/kustomization.yaml
|
||||||
- path: manager_webhook_patch.yaml
|
- path: manager_webhook_patch.yaml
|
||||||
|
|
||||||
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
|
|
||||||
# Uncomment the following replacements to add the cert-manager CA injection annotations
|
|
||||||
replacements:
|
replacements:
|
||||||
- source: # Uncomment the following block if you have any webhook
|
- source:
|
||||||
|
kind: Service
|
||||||
|
version: v1
|
||||||
|
name: control-plane
|
||||||
|
fieldPath: .metadata.name
|
||||||
|
targets:
|
||||||
|
- select:
|
||||||
|
kind: Deployment
|
||||||
|
group: apps
|
||||||
|
version: v1
|
||||||
|
name: control-plane
|
||||||
|
fieldPaths:
|
||||||
|
- .spec.template.spec.containers.*.env.[name=CONTROL_PLANE_SERVICE_NAME].value
|
||||||
|
- source:
|
||||||
kind: Service
|
kind: Service
|
||||||
version: v1
|
version: v1
|
||||||
name: webhook-service
|
name: webhook-service
|
||||||
fieldPath: .metadata.name # Name of the service
|
fieldPath: .metadata.name
|
||||||
targets:
|
targets:
|
||||||
- select:
|
- select:
|
||||||
kind: Certificate
|
kind: Certificate
|
||||||
group: cert-manager.io
|
group: cert-manager.io
|
||||||
version: v1
|
version: v1
|
||||||
|
name: serving-cert
|
||||||
fieldPaths:
|
fieldPaths:
|
||||||
- .spec.dnsNames.0
|
- .spec.dnsNames.0
|
||||||
- .spec.dnsNames.1
|
- .spec.dnsNames.1
|
||||||
|
@ -74,6 +85,7 @@ replacements:
|
||||||
kind: Certificate
|
kind: Certificate
|
||||||
group: cert-manager.io
|
group: cert-manager.io
|
||||||
version: v1
|
version: v1
|
||||||
|
name: serving-cert
|
||||||
fieldPaths:
|
fieldPaths:
|
||||||
- .spec.dnsNames.0
|
- .spec.dnsNames.0
|
||||||
- .spec.dnsNames.1
|
- .spec.dnsNames.1
|
||||||
|
|
1
config/dev/.gitignore
vendored
Normal file
1
config/dev/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
studio-credentials-secret.yaml
|
|
@ -1,3 +1,4 @@
|
||||||
|
---
|
||||||
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
|
apiVersion: supabase.k8s.icb4dc0.de/v1alpha1
|
||||||
kind: APIGateway
|
kind: APIGateway
|
||||||
metadata:
|
metadata:
|
||||||
|
@ -5,8 +6,44 @@ metadata:
|
||||||
app.kubernetes.io/name: supabase-operator
|
app.kubernetes.io/name: supabase-operator
|
||||||
app.kubernetes.io/managed-by: kustomize
|
app.kubernetes.io/managed-by: kustomize
|
||||||
name: gateway-sample
|
name: gateway-sample
|
||||||
|
namespace: supabase-demo
|
||||||
spec:
|
spec:
|
||||||
apiEndpoint:
|
apiEndpoint:
|
||||||
jwks:
|
jwks:
|
||||||
name: core-sample-jwt
|
name: core-sample-jwt
|
||||||
key: jwks.json
|
key: jwks.json
|
||||||
|
dashboardEndpoint:
|
||||||
|
auth:
|
||||||
|
oauth2:
|
||||||
|
tokenEndpoint: "https://login.microsoftonline.com/f4e80111-1571-477a-b56d-c5fe517676b7/oauth2/token"
|
||||||
|
authorizationEndpoint: "https://login.microsoftonline.com/f4e80111-1571-477a-b56d-c5fe517676b7/oauth2/authorize"
|
||||||
|
clientId: 3528016b-f6e3-49be-8fb3-f9a9a2ab6c3f
|
||||||
|
scopes:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- email
|
||||||
|
clientSecretRef:
|
||||||
|
name: studio-sample-oauth2
|
||||||
|
key: clientSecret
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: certificate
|
||||||
|
app.kubernetes.io/instance: dashboard-tls
|
||||||
|
app.kubernetes.io/component: certificate
|
||||||
|
app.kubernetes.io/created-by: supabase-operator
|
||||||
|
app.kubernetes.io/part-of: supabase-operator
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
name: dashboard-tls
|
||||||
|
namespace: supabase-demo
|
||||||
|
spec:
|
||||||
|
dnsNames:
|
||||||
|
- gateway-sample-envoy.supabase-demo.svc
|
||||||
|
- gateway-sample-envoy.supabase-demo.svc.cluster.local
|
||||||
|
- localhost:3000
|
||||||
|
issuerRef:
|
||||||
|
kind: Issuer
|
||||||
|
name: selfsigned-issuer
|
||||||
|
secretName: dashboard-tls-cert
|
||||||
|
|
|
@ -3,6 +3,7 @@ apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
name: pgsodium-config
|
name: pgsodium-config
|
||||||
|
namespace: supabase-demo
|
||||||
data:
|
data:
|
||||||
pgsodium_getkey.sh: |
|
pgsodium_getkey.sh: |
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
@ -18,6 +19,7 @@ apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: pgsodium-key
|
name: pgsodium-key
|
||||||
|
namespace: supabase-demo
|
||||||
data:
|
data:
|
||||||
# Generate a 32-byte key
|
# Generate a 32-byte key
|
||||||
# head -c 32 /dev/urandom | od -A n -t x1 | tr -d ' \n' | base64
|
# head -c 32 /dev/urandom | od -A n -t x1 | tr -d ' \n' | base64
|
||||||
|
@ -27,6 +29,7 @@ apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: supabase-admin-credentials
|
name: supabase-admin-credentials
|
||||||
|
namespace: supabase-demo
|
||||||
labels:
|
labels:
|
||||||
cnpg.io/reload: "true"
|
cnpg.io/reload: "true"
|
||||||
type: kubernetes.io/basic-auth
|
type: kubernetes.io/basic-auth
|
||||||
|
@ -38,6 +41,7 @@ apiVersion: postgresql.cnpg.io/v1
|
||||||
kind: Cluster
|
kind: Cluster
|
||||||
metadata:
|
metadata:
|
||||||
name: cluster-example
|
name: cluster-example
|
||||||
|
namespace: supabase-demo
|
||||||
spec:
|
spec:
|
||||||
instances: 1
|
instances: 1
|
||||||
imageName: ghcr.io/supabase/postgres:15.8.1.021
|
imageName: ghcr.io/supabase/postgres:15.8.1.021
|
|
@ -2,6 +2,7 @@ apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: supabase-demo-credentials
|
name: supabase-demo-credentials
|
||||||
|
namespace: supabase-demo
|
||||||
stringData:
|
stringData:
|
||||||
url: postgresql://supabase_admin:1n1t-R00t!@cluster-example-rw.supabase-demo:5432/app
|
url: postgresql://supabase_admin:1n1t-R00t!@cluster-example-rw.supabase-demo:5432/app
|
||||||
---
|
---
|
||||||
|
@ -12,6 +13,7 @@ metadata:
|
||||||
app.kubernetes.io/name: supabase-operator
|
app.kubernetes.io/name: supabase-operator
|
||||||
app.kubernetes.io/managed-by: kustomize
|
app.kubernetes.io/managed-by: kustomize
|
||||||
name: core-sample
|
name: core-sample
|
||||||
|
namespace: supabase-demo
|
||||||
spec:
|
spec:
|
||||||
# public URL of the Supabase instance (API)
|
# public URL of the Supabase instance (API)
|
||||||
# normally the Ingress/HTTPRoute endpoint
|
# normally the Ingress/HTTPRoute endpoint
|
||||||
|
|
|
@ -6,6 +6,7 @@ metadata:
|
||||||
app.kubernetes.io/name: supabase-operator
|
app.kubernetes.io/name: supabase-operator
|
||||||
app.kubernetes.io/managed-by: kustomize
|
app.kubernetes.io/managed-by: kustomize
|
||||||
name: dashboard-sample
|
name: dashboard-sample
|
||||||
|
namespace: supabase-demo
|
||||||
spec:
|
spec:
|
||||||
db:
|
db:
|
||||||
host: cluster-example-rw.supabase-demo.svc
|
host: cluster-example-rw.supabase-demo.svc
|
||||||
|
|
|
@ -4,8 +4,11 @@ kind: Kustomization
|
||||||
resources:
|
resources:
|
||||||
- https://github.com/cert-manager/cert-manager/releases/download/v1.16.3/cert-manager.yaml
|
- https://github.com/cert-manager/cert-manager/releases/download/v1.16.3/cert-manager.yaml
|
||||||
- https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.25.0/cnpg-1.25.0.yaml
|
- https://github.com/cloudnative-pg/cloudnative-pg/releases/download/v1.25.0/cnpg-1.25.0.yaml
|
||||||
|
- namespace.yaml
|
||||||
|
- cnpg-cluster.yaml
|
||||||
- minio.yaml
|
- minio.yaml
|
||||||
- ../default
|
- ../default
|
||||||
|
- studio-credentials-secret.yaml
|
||||||
- core.yaml
|
- core.yaml
|
||||||
- apigateway.yaml
|
- apigateway.yaml
|
||||||
- dashboard.yaml
|
- dashboard.yaml
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
- op: replace
|
|
||||||
path: /spec/replicas
|
|
||||||
value: 1
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Deploys a new Namespace for the MinIO Pod
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
metadata:
|
||||||
|
|
4
config/dev/namespace.yaml
Normal file
4
config/dev/namespace.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: supabase-demo
|
|
@ -3,6 +3,7 @@ apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: storage-s3-credentials
|
name: storage-s3-credentials
|
||||||
|
namespace: supabase-demo
|
||||||
stringData:
|
stringData:
|
||||||
accessKeyId: FPxTAFL7NaubjPgIGBo3
|
accessKeyId: FPxTAFL7NaubjPgIGBo3
|
||||||
secretAccessKey: 7F437pPe84QcoocD3MWdAIVBU3oXonhVHxK645tm
|
secretAccessKey: 7F437pPe84QcoocD3MWdAIVBU3oXonhVHxK645tm
|
||||||
|
@ -14,6 +15,7 @@ metadata:
|
||||||
app.kubernetes.io/name: supabase-operator
|
app.kubernetes.io/name: supabase-operator
|
||||||
app.kubernetes.io/managed-by: kustomize
|
app.kubernetes.io/managed-by: kustomize
|
||||||
name: storage-sample
|
name: storage-sample
|
||||||
|
namespace: supabase-demo
|
||||||
spec:
|
spec:
|
||||||
api:
|
api:
|
||||||
s3Backend:
|
s3Backend:
|
||||||
|
|
8
config/dev/studio-credentials-secret.yaml
Normal file
8
config/dev/studio-credentials-secret.yaml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: studio-sample-oauth2
|
||||||
|
namespace: supabase-demo
|
||||||
|
stringData:
|
||||||
|
clientSecret: "G9r8Q~o4LJRlTQwPpdCBaZLsWdhUxM_02Y_XBcEr"
|
|
@ -20,26 +20,6 @@ spec:
|
||||||
labels:
|
labels:
|
||||||
control-plane: controller-manager
|
control-plane: controller-manager
|
||||||
spec:
|
spec:
|
||||||
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
|
|
||||||
# according to the platforms which are supported by your solution.
|
|
||||||
# It is considered best practice to support multiple architectures. You can
|
|
||||||
# build your manager image using the makefile target docker-buildx.
|
|
||||||
# affinity:
|
|
||||||
# nodeAffinity:
|
|
||||||
# requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
# nodeSelectorTerms:
|
|
||||||
# - matchExpressions:
|
|
||||||
# - key: kubernetes.io/arch
|
|
||||||
# operator: In
|
|
||||||
# values:
|
|
||||||
# - amd64
|
|
||||||
# - arm64
|
|
||||||
# - ppc64le
|
|
||||||
# - s390x
|
|
||||||
# - key: kubernetes.io/os
|
|
||||||
# operator: In
|
|
||||||
# values:
|
|
||||||
# - linux
|
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
seccompProfile:
|
seccompProfile:
|
||||||
|
@ -56,6 +36,10 @@ spec:
|
||||||
valueFrom:
|
valueFrom:
|
||||||
fieldRef:
|
fieldRef:
|
||||||
fieldPath: metadata.namespace
|
fieldPath: metadata.namespace
|
||||||
|
- name: TLS_CA_CERT
|
||||||
|
value: /etc/supabase/operator/certs/tls.crt
|
||||||
|
- name: TLS_CA_KEY
|
||||||
|
value: /etc/supabase/operator/certs/tls.key
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
capabilities:
|
capabilities:
|
||||||
|
@ -73,8 +57,6 @@ spec:
|
||||||
port: 8081
|
port: 8081
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
# TODO(user): Configure the resources accordingly based on the project requirements.
|
|
||||||
# More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 150m
|
cpu: 150m
|
||||||
|
@ -82,5 +64,12 @@ spec:
|
||||||
requests:
|
requests:
|
||||||
cpu: 10m
|
cpu: 10m
|
||||||
memory: 64Mi
|
memory: 64Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: tls-certs
|
||||||
|
mountPath: /etc/supabase/operator/certs
|
||||||
|
volumes:
|
||||||
|
- name: tls-certs
|
||||||
|
secret:
|
||||||
|
secretName: control-plane-ca-cert-tls
|
||||||
serviceAccountName: controller-manager
|
serviceAccountName: controller-manager
|
||||||
terminationGracePeriodSeconds: 10
|
terminationGracePeriodSeconds: 10
|
||||||
|
|
|
@ -4,6 +4,17 @@ kind: ClusterRole
|
||||||
metadata:
|
metadata:
|
||||||
name: control-plane-role
|
name: control-plane-role
|
||||||
rules:
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- secrets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- update
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- supabase.k8s.icb4dc0.de
|
- supabase.k8s.icb4dc0.de
|
||||||
resources:
|
resources:
|
||||||
|
@ -12,6 +23,8 @@ rules:
|
||||||
- get
|
- get
|
||||||
- list
|
- list
|
||||||
- watch
|
- watch
|
||||||
|
- update
|
||||||
|
|
||||||
- apiGroups:
|
- apiGroups:
|
||||||
- supabase.k8s.icb4dc0.de
|
- supabase.k8s.icb4dc0.de
|
||||||
resources:
|
resources:
|
||||||
|
|
|
@ -4,7 +4,6 @@ namespace: supabase-demo
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- cnpg-cluster.yaml
|
|
||||||
- supabase_v1alpha1_core.yaml
|
- supabase_v1alpha1_core.yaml
|
||||||
- supabase_v1alpha1_apigateway.yaml
|
- supabase_v1alpha1_apigateway.yaml
|
||||||
- supabase_v1alpha1_dashboard.yaml
|
- supabase_v1alpha1_dashboard.yaml
|
||||||
|
|
|
@ -7,3 +7,6 @@ apiVersion: ctlptl.dev/v1alpha1
|
||||||
kind: Cluster
|
kind: Cluster
|
||||||
product: kind
|
product: kind
|
||||||
registry: ctlptl-registry
|
registry: ctlptl-registry
|
||||||
|
kindV1Alpha4Cluster:
|
||||||
|
networking:
|
||||||
|
ipFamily: dual
|
||||||
|
|
23
go.mod
23
go.mod
|
@ -5,6 +5,8 @@ go 1.23.5
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/kong v1.6.0
|
github.com/alecthomas/kong v1.6.0
|
||||||
github.com/envoyproxy/go-control-plane v0.13.1
|
github.com/envoyproxy/go-control-plane v0.13.1
|
||||||
|
github.com/go-logr/logr v1.4.2
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0
|
||||||
github.com/jackc/pgx/v5 v5.7.1
|
github.com/jackc/pgx/v5 v5.7.1
|
||||||
github.com/lestrrat-go/jwx/v2 v2.1.3
|
github.com/lestrrat-go/jwx/v2 v2.1.3
|
||||||
github.com/magefile/mage v1.15.0
|
github.com/magefile/mage v1.15.0
|
||||||
|
@ -12,8 +14,8 @@ require (
|
||||||
github.com/onsi/gomega v1.35.1
|
github.com/onsi/gomega v1.35.1
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||||
google.golang.org/grpc v1.65.0
|
google.golang.org/grpc v1.70.0
|
||||||
google.golang.org/protobuf v1.36.3
|
google.golang.org/protobuf v1.36.4
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/api v0.32.1
|
k8s.io/api v0.32.1
|
||||||
k8s.io/apimachinery v0.32.1
|
k8s.io/apimachinery v0.32.1
|
||||||
|
@ -22,7 +24,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cel.dev/expr v0.18.0 // indirect
|
cel.dev/expr v0.19.0 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
@ -30,7 +32,7 @@ require (
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect
|
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||||
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
|
||||||
|
@ -39,7 +41,6 @@ require (
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-logr/zapr v1.3.0 // indirect
|
github.com/go-logr/zapr v1.3.0 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
@ -84,12 +85,12 @@ require (
|
||||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/crypto v0.32.0 // indirect
|
golang.org/x/crypto v0.32.0 // indirect
|
||||||
|
@ -102,8 +103,8 @@ require (
|
||||||
golang.org/x/time v0.9.0 // indirect
|
golang.org/x/time v0.9.0 // indirect
|
||||||
golang.org/x/tools v0.26.0 // indirect
|
golang.org/x/tools v0.26.0 // indirect
|
||||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
k8s.io/apiextensions-apiserver v0.32.1 // indirect
|
k8s.io/apiextensions-apiserver v0.32.1 // indirect
|
||||||
|
|
44
go.sum
44
go.sum
|
@ -1,5 +1,5 @@
|
||||||
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
|
cel.dev/expr v0.19.0 h1:lXuo+nDhpyJSpWxpPVi5cPUwzKb+dsdOiw6IreM5yt0=
|
||||||
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
cel.dev/expr v0.19.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE=
|
github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE=
|
||||||
|
@ -20,8 +20,8 @@ github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMr
|
||||||
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw=
|
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI=
|
||||||
github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
@ -82,6 +82,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY
|
||||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
@ -180,18 +182,20 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||||
|
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||||
|
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
@ -247,14 +251,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
|
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
|
||||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|
60
internal/certs/format.go
Normal file
60
internal/certs/format.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
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 certs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certificateBlockType = "CERTIFICATE"
|
||||||
|
privateKeyBlockType = "PRIVATE KEY"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EncodePublicKeyToPEM(derBytes []byte) []byte {
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: certificateBlockType, Bytes: derBytes})
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncodePrivateKeyToPEM(privateKeyBytes []byte) []byte {
|
||||||
|
return pem.EncodeToMemory(&pem.Block{Type: privateKeyBlockType, Bytes: privateKeyBytes})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCertResult(derBytes, privateKeyBytes []byte) (CertResult, error) {
|
||||||
|
pemEncodedPublicKey := EncodePublicKeyToPEM(derBytes)
|
||||||
|
pemEncodedPrivateKey := EncodePrivateKeyToPEM(privateKeyBytes)
|
||||||
|
|
||||||
|
cert, err := tls.X509KeyPair(pemEncodedPublicKey, pemEncodedPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return CertResult{}, fmt.Errorf("failed to create TLS cert based on x509 key pair: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := CertResult{
|
||||||
|
ServerCert: cert,
|
||||||
|
PublicKey: pemEncodedPublicKey,
|
||||||
|
PrivateKey: pemEncodedPrivateKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CertResult struct {
|
||||||
|
ServerCert tls.Certificate
|
||||||
|
PublicKey []byte
|
||||||
|
PrivateKey []byte
|
||||||
|
}
|
122
internal/certs/generate.go
Normal file
122
internal/certs/generate.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
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 certs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ServerCert(
|
||||||
|
commonName string,
|
||||||
|
dnsNames []string,
|
||||||
|
ca tls.Certificate,
|
||||||
|
) (result CertResult, err error) {
|
||||||
|
serial, err := generateSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("failed to generate private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certTemplate := x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: commonName,
|
||||||
|
},
|
||||||
|
DNSNames: dnsNames,
|
||||||
|
NotBefore: time.Now().Add(-1 * time.Hour),
|
||||||
|
NotAfter: time.Now().AddDate(0, 1, 0),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyEncipherment | x509.KeyUsageContentCommitment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
}
|
||||||
|
|
||||||
|
var caCrt *x509.Certificate
|
||||||
|
if caCrt, err = x509.ParseCertificate(ca.Certificate[0]); err != nil {
|
||||||
|
return result, fmt.Errorf("failed parse CA cert: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var derBytes []byte
|
||||||
|
if derBytes, err = x509.CreateCertificate(rand.Reader, &certTemplate, caCrt, &privateKey.PublicKey, ca.PrivateKey); err != nil {
|
||||||
|
return result, fmt.Errorf("failed to create signed certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKeyBytes []byte
|
||||||
|
if privateKeyBytes, err = x509.MarshalPKCS8PrivateKey(privateKey); err != nil {
|
||||||
|
return result, fmt.Errorf("failed to marshal private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newCertResult(derBytes, privateKeyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientCert(
|
||||||
|
commonName string,
|
||||||
|
ca tls.Certificate,
|
||||||
|
) (result CertResult, err error) {
|
||||||
|
serial, err := generateSerialNumber()
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("failed to generate private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
certTemplate := x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: commonName,
|
||||||
|
},
|
||||||
|
NotBefore: time.Now().Add(-1 * time.Hour),
|
||||||
|
NotAfter: time.Now().AddDate(0, 0, 7),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyEncipherment | x509.KeyUsageContentCommitment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
||||||
|
}
|
||||||
|
|
||||||
|
var caCrt *x509.Certificate
|
||||||
|
if caCrt, err = x509.ParseCertificate(ca.Certificate[0]); err != nil {
|
||||||
|
return result, fmt.Errorf("failed parse CA cert: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var derBytes []byte
|
||||||
|
if derBytes, err = x509.CreateCertificate(rand.Reader, &certTemplate, caCrt, &privateKey.PublicKey, ca.PrivateKey); err != nil {
|
||||||
|
return result, fmt.Errorf("failed to create signed certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var privateKeyBytes []byte
|
||||||
|
if privateKeyBytes, err = x509.MarshalPKCS8PrivateKey(privateKey); err != nil {
|
||||||
|
return result, fmt.Errorf("failed to marshal private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newCertResult(derBytes, privateKeyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSerialNumber() (*big.Int, error) {
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
return rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
}
|
|
@ -19,10 +19,13 @@ package controller
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -41,6 +44,7 @@ import (
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/certs"
|
||||||
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
"code.icb4dc0.de/prskr/supabase-operator/internal/meta"
|
||||||
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
)
|
)
|
||||||
|
@ -64,6 +68,7 @@ func init() {
|
||||||
type APIGatewayReconciler struct {
|
type APIGatewayReconciler struct {
|
||||||
client.Client
|
client.Client
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
|
CACert tls.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||||
|
@ -85,6 +90,14 @@ func (r *APIGatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.reconcileHmacSecret(ctx, &gateway); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to ensure HMAC token secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconcileClientCertSecret(ctx, &gateway); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("failed to ensure client certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if jwksHash, err = r.reconcileJwksSecret(ctx, &gateway); err != nil {
|
if jwksHash, err = r.reconcileJwksSecret(ctx, &gateway); err != nil {
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
@ -105,7 +118,8 @@ func (r *APIGatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctrl.Result{}, nil
|
// requeue after 15 minutes to watch for expiring certificates
|
||||||
|
return ctrl.Result{RequeueAfter: 15 * time.Minute}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupWithManager sets up the controller with the Manager.
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
|
@ -144,6 +158,8 @@ func (r *APIGatewayReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&supabasev1alpha1.APIGateway{}).
|
For(&supabasev1alpha1.APIGateway{}).
|
||||||
Named("apigateway").
|
Named("apigateway").
|
||||||
|
// watch for the HMAC & client cert secrets
|
||||||
|
Owns(new(corev1.Secret)).
|
||||||
Owns(new(corev1.ConfigMap)).
|
Owns(new(corev1.ConfigMap)).
|
||||||
Owns(new(appsv1.Deployment)).
|
Owns(new(appsv1.Deployment)).
|
||||||
Owns(new(corev1.Service)).
|
Owns(new(corev1.Service)).
|
||||||
|
@ -219,6 +235,115 @@ func (r *APIGatewayReconciler) reconcileJwksSecret(
|
||||||
return hex.EncodeToString(HashBytes(jwksRaw)), nil
|
return hex.EncodeToString(HashBytes(jwksRaw)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *APIGatewayReconciler) reconcileHmacSecret(
|
||||||
|
ctx context.Context,
|
||||||
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
|
) error {
|
||||||
|
const hmacSecretLength = 32
|
||||||
|
|
||||||
|
serviceCfg := supabase.ServiceConfig.Envoy
|
||||||
|
|
||||||
|
hmacSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: serviceCfg.HmacSecretName(gateway),
|
||||||
|
Namespace: gateway.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, hmacSecret, func() error {
|
||||||
|
if hmacSecret.Data == nil {
|
||||||
|
hmacSecret.Data = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := hmacSecret.Data[serviceCfg.Defaults.HmacSecretKey]; !ok {
|
||||||
|
secret := make([]byte, hmacSecretLength)
|
||||||
|
if n, err := rand.Read(secret); err != nil {
|
||||||
|
return fmt.Errorf("failed to generate HMAC token secret: %w", err)
|
||||||
|
} else if n != hmacSecretLength {
|
||||||
|
return fmt.Errorf("failed to generate HMAC token secret: not enough bytes generated")
|
||||||
|
}
|
||||||
|
|
||||||
|
hmacSecret.Data[serviceCfg.Defaults.HmacSecretKey] = secret
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(gateway, hmacSecret, r.Scheme); err != nil {
|
||||||
|
return fmt.Errorf("failed to set controller reference: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *APIGatewayReconciler) reconcileClientCertSecret(
|
||||||
|
ctx context.Context,
|
||||||
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
|
) error {
|
||||||
|
var (
|
||||||
|
logger = log.FromContext(ctx)
|
||||||
|
clientCertSecret = &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: supabase.ServiceConfig.Envoy.ControlPlaneClientCertSecretName(gateway),
|
||||||
|
Namespace: gateway.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, clientCertSecret, func() (err error) {
|
||||||
|
clientCertSecret.Type = corev1.SecretTypeTLS
|
||||||
|
if clientCertSecret.Data == nil {
|
||||||
|
clientCertSecret.Data = make(map[string][]byte, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertBytes := certs.EncodePublicKeyToPEM(r.CACert.Certificate[0])
|
||||||
|
clientCertSecret.Data["ca.crt"] = caCertBytes
|
||||||
|
|
||||||
|
var (
|
||||||
|
cert = clientCertSecret.Data[corev1.TLSCertKey]
|
||||||
|
privateKey = clientCertSecret.Data[corev1.TLSPrivateKeyKey]
|
||||||
|
clientCert tls.Certificate
|
||||||
|
)
|
||||||
|
|
||||||
|
var requireRenewal bool
|
||||||
|
if cert != nil && privateKey != nil {
|
||||||
|
if clientCert, err = tls.X509KeyPair(cert, privateKey); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse server certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
renewGracePeriod := time.Duration(float64(clientCert.Leaf.NotAfter.Sub(clientCert.Leaf.NotBefore)) * 0.1)
|
||||||
|
if clientCert.Leaf.NotAfter.Before(time.Now().Add(-renewGracePeriod)) {
|
||||||
|
logger.Info("Envoy control-plane client certificate requires renewal",
|
||||||
|
"not_after", clientCert.Leaf.NotAfter,
|
||||||
|
"renew_grace_period", renewGracePeriod,
|
||||||
|
)
|
||||||
|
requireRenewal = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Info("Client cert is not set creating a new one")
|
||||||
|
requireRenewal = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if requireRenewal {
|
||||||
|
if certResult, err := certs.ClientCert(strings.Join([]string{gateway.Name, gateway.Namespace}, ":"), r.CACert); err != nil {
|
||||||
|
return fmt.Errorf("failed to generate server certificate: %w", err)
|
||||||
|
} else {
|
||||||
|
clientCert = certResult.ServerCert
|
||||||
|
clientCertSecret.Data[corev1.TLSCertKey] = certResult.PublicKey
|
||||||
|
clientCertSecret.Data[corev1.TLSPrivateKeyKey] = certResult.PrivateKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controllerutil.SetControllerReference(gateway, clientCertSecret, r.Scheme); err != nil {
|
||||||
|
return fmt.Errorf("failed to set controller reference: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *APIGatewayReconciler) reconcileEnvoyConfig(
|
func (r *APIGatewayReconciler) reconcileEnvoyConfig(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
gateway *supabasev1alpha1.APIGateway,
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
|
@ -291,6 +416,12 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
||||||
gateway *supabasev1alpha1.APIGateway,
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
configHash, jwksHash string,
|
configHash, jwksHash string,
|
||||||
) error {
|
) error {
|
||||||
|
const (
|
||||||
|
configVolumeName = "config"
|
||||||
|
controlPlaneTlsVolumeName = "cp-tls"
|
||||||
|
dashboardTlsVolumeName = "dashboard-tls"
|
||||||
|
apiTlsVolumeName = "api-tls"
|
||||||
|
)
|
||||||
envoyDeployment := &appsv1.Deployment{
|
envoyDeployment := &appsv1.Deployment{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
||||||
|
@ -317,6 +448,131 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
||||||
|
|
||||||
envoyDeployment.Spec.Replicas = envoySpec.WorkloadTemplate.ReplicaCount()
|
envoyDeployment.Spec.Replicas = envoySpec.WorkloadTemplate.ReplicaCount()
|
||||||
|
|
||||||
|
configVolumeProjectionSources := []corev1.VolumeProjection{
|
||||||
|
{
|
||||||
|
ConfigMap: &corev1.ConfigMapProjection{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
||||||
|
},
|
||||||
|
Items: []corev1.KeyToPath{
|
||||||
|
{
|
||||||
|
Key: "config.yaml",
|
||||||
|
Path: "config.yaml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Secret: &corev1.SecretProjection{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: gateway.Spec.ApiEndpoint.JWKSSelector.Name,
|
||||||
|
},
|
||||||
|
Items: []corev1.KeyToPath{{
|
||||||
|
Key: gateway.Spec.ApiEndpoint.JWKSSelector.Key,
|
||||||
|
Path: "jwks.json",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Secret: &corev1.SecretProjection{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: serviceCfg.ControlPlaneClientCertSecretName(gateway),
|
||||||
|
},
|
||||||
|
Items: []corev1.KeyToPath{
|
||||||
|
{
|
||||||
|
Key: "ca.crt",
|
||||||
|
Path: "certs/cp/ca.crt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "tls.crt",
|
||||||
|
Path: "certs/cp/tls.crt",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "tls.key",
|
||||||
|
Path: "certs/cp/tls.key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2Spec := gateway.Spec.DashboardEndpoint.OAuth2(); oauth2Spec != nil {
|
||||||
|
configVolumeProjectionSources = append(configVolumeProjectionSources, corev1.VolumeProjection{
|
||||||
|
Secret: &corev1.SecretProjection{
|
||||||
|
LocalObjectReference: corev1.LocalObjectReference{
|
||||||
|
Name: oauth2Spec.ClientSecretRef.Name,
|
||||||
|
},
|
||||||
|
Items: []corev1.KeyToPath{{
|
||||||
|
Key: oauth2Spec.ClientSecretRef.Key,
|
||||||
|
Path: serviceCfg.Defaults.OAuth2ClientSecretKey,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeMounts := []corev1.VolumeMount{
|
||||||
|
{
|
||||||
|
Name: configVolumeName,
|
||||||
|
ReadOnly: true,
|
||||||
|
MountPath: "/etc/envoy",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
volumes := []corev1.Volume{
|
||||||
|
{
|
||||||
|
Name: configVolumeName,
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
Projected: &corev1.ProjectedVolumeSource{
|
||||||
|
Sources: configVolumeProjectionSources,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: controlPlaneTlsVolumeName,
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
Secret: &corev1.SecretVolumeSource{
|
||||||
|
SecretName: serviceCfg.ControlPlaneClientCertSecretName(gateway),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsSpec := gateway.Spec.ApiEndpoint.TLSSpec(); tlsSpec != nil {
|
||||||
|
volumes = append(volumes, corev1.Volume{
|
||||||
|
Name: apiTlsVolumeName,
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
Secret: &corev1.SecretVolumeSource{
|
||||||
|
SecretName: tlsSpec.Cert.SecretName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
||||||
|
Name: dashboardTlsVolumeName,
|
||||||
|
ReadOnly: true,
|
||||||
|
MountPath: "/etc/envoy/certs/api",
|
||||||
|
SubPath: "certs/api",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsSpec := gateway.Spec.DashboardEndpoint.TLSSpec(); tlsSpec != nil {
|
||||||
|
volumes = append(volumes, corev1.Volume{
|
||||||
|
Name: dashboardTlsVolumeName,
|
||||||
|
VolumeSource: corev1.VolumeSource{
|
||||||
|
Secret: &corev1.SecretVolumeSource{
|
||||||
|
SecretName: tlsSpec.Cert.SecretName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
||||||
|
Name: dashboardTlsVolumeName,
|
||||||
|
ReadOnly: true,
|
||||||
|
MountPath: "/etc/envoy/certs/dashboard",
|
||||||
|
SubPath: "certs/dashboard",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
envoyDeployment.Spec.Template = corev1.PodTemplateSpec{
|
envoyDeployment.Spec.Template = corev1.PodTemplateSpec{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
|
@ -333,16 +589,21 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
||||||
Name: "envoy-proxy",
|
Name: "envoy-proxy",
|
||||||
Image: envoySpec.WorkloadTemplate.Image(supabase.Images.Envoy.String()),
|
Image: envoySpec.WorkloadTemplate.Image(supabase.Images.Envoy.String()),
|
||||||
ImagePullPolicy: envoySpec.WorkloadTemplate.ImagePullPolicy(),
|
ImagePullPolicy: envoySpec.WorkloadTemplate.ImagePullPolicy(),
|
||||||
Args: []string{"-c /etc/envoy/config.yaml"},
|
Args: []string{"-c /etc/envoy/config.yaml"}, // , "--component-log-level", "upstream:debug,connection:debug"
|
||||||
Ports: []corev1.ContainerPort{
|
Ports: []corev1.ContainerPort{
|
||||||
{
|
{
|
||||||
Name: "http",
|
Name: serviceCfg.Defaults.StudioPortName,
|
||||||
ContainerPort: 8000,
|
ContainerPort: serviceCfg.Defaults.StudioPort,
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: serviceCfg.Defaults.ApiPortName,
|
||||||
|
ContainerPort: serviceCfg.Defaults.ApiPort,
|
||||||
Protocol: corev1.ProtocolTCP,
|
Protocol: corev1.ProtocolTCP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "admin",
|
Name: "admin",
|
||||||
ContainerPort: 19000,
|
ContainerPort: serviceCfg.Defaults.AdminPort,
|
||||||
Protocol: corev1.ProtocolTCP,
|
Protocol: corev1.ProtocolTCP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -371,49 +632,11 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment(
|
||||||
},
|
},
|
||||||
SecurityContext: envoySpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.UID, serviceCfg.Defaults.GID),
|
SecurityContext: envoySpec.WorkloadTemplate.ContainerSecurityContext(serviceCfg.Defaults.UID, serviceCfg.Defaults.GID),
|
||||||
Resources: envoySpec.WorkloadTemplate.Resources(),
|
Resources: envoySpec.WorkloadTemplate.Resources(),
|
||||||
VolumeMounts: envoySpec.WorkloadTemplate.AdditionalVolumeMounts(
|
VolumeMounts: envoySpec.WorkloadTemplate.AdditionalVolumeMounts(volumeMounts...),
|
||||||
corev1.VolumeMount{
|
|
||||||
Name: "config",
|
|
||||||
ReadOnly: true,
|
|
||||||
MountPath: "/etc/envoy",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
SecurityContext: envoySpec.WorkloadTemplate.PodSecurityContext(),
|
SecurityContext: envoySpec.WorkloadTemplate.PodSecurityContext(),
|
||||||
Volumes: []corev1.Volume{
|
Volumes: volumes,
|
||||||
{
|
|
||||||
Name: "config",
|
|
||||||
VolumeSource: corev1.VolumeSource{
|
|
||||||
Projected: &corev1.ProjectedVolumeSource{
|
|
||||||
Sources: []corev1.VolumeProjection{
|
|
||||||
{
|
|
||||||
ConfigMap: &corev1.ConfigMapProjection{
|
|
||||||
LocalObjectReference: corev1.LocalObjectReference{
|
|
||||||
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
|
||||||
},
|
|
||||||
Items: []corev1.KeyToPath{{
|
|
||||||
Key: "config.yaml",
|
|
||||||
Path: "config.yaml",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Secret: &corev1.SecretProjection{
|
|
||||||
LocalObjectReference: corev1.LocalObjectReference{
|
|
||||||
Name: gateway.Spec.ApiEndpoint.JWKSSelector.Name,
|
|
||||||
},
|
|
||||||
Items: []corev1.KeyToPath{{
|
|
||||||
Key: gateway.Spec.ApiEndpoint.JWKSSelector.Key,
|
|
||||||
Path: "jwks.json",
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -431,12 +654,15 @@ func (r *APIGatewayReconciler) reconcileEnvoyService(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
gateway *supabasev1alpha1.APIGateway,
|
gateway *supabasev1alpha1.APIGateway,
|
||||||
) error {
|
) error {
|
||||||
envoyService := &corev1.Service{
|
var (
|
||||||
|
serviceCfg = supabase.ServiceConfig.Envoy
|
||||||
|
envoyService = &corev1.Service{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
Name: supabase.ServiceConfig.Envoy.ObjectName(gateway),
|
||||||
Namespace: gateway.Namespace,
|
Namespace: gateway.Namespace,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, envoyService, func() error {
|
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, envoyService, func() error {
|
||||||
envoyService.Labels = MergeLabels(objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag), gateway.Labels)
|
envoyService.Labels = MergeLabels(objectLabels(gateway, "envoy", "api-gateway", supabase.Images.Envoy.Tag), gateway.Labels)
|
||||||
|
@ -445,11 +671,18 @@ func (r *APIGatewayReconciler) reconcileEnvoyService(
|
||||||
Selector: selectorLabels(gateway, "envoy"),
|
Selector: selectorLabels(gateway, "envoy"),
|
||||||
Ports: []corev1.ServicePort{
|
Ports: []corev1.ServicePort{
|
||||||
{
|
{
|
||||||
Name: "rest",
|
Name: serviceCfg.Defaults.StudioPortName,
|
||||||
Protocol: corev1.ProtocolTCP,
|
Protocol: corev1.ProtocolTCP,
|
||||||
AppProtocol: ptrOf("http"),
|
AppProtocol: ptrOf("http"),
|
||||||
Port: 8000,
|
Port: serviceCfg.Defaults.StudioPort,
|
||||||
TargetPort: intstr.IntOrString{IntVal: 8000},
|
TargetPort: intstr.IntOrString{IntVal: serviceCfg.Defaults.StudioPort},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: serviceCfg.Defaults.ApiPortName,
|
||||||
|
Protocol: corev1.ProtocolTCP,
|
||||||
|
AppProtocol: ptrOf("http"),
|
||||||
|
Port: serviceCfg.Defaults.ApiPort,
|
||||||
|
TargetPort: intstr.IntOrString{IntVal: serviceCfg.Defaults.ApiPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -192,7 +192,7 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
||||||
SuccessThreshold: 2,
|
SuccessThreshold: 2,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/health",
|
Path: svcCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: svcCfg.Defaults.APIPort},
|
Port: intstr.IntOrString{IntVal: svcCfg.Defaults.APIPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -203,7 +203,7 @@ func (r *CoreAuthReconciler) reconcileAuthDeployment(
|
||||||
TimeoutSeconds: 3,
|
TimeoutSeconds: 3,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/health",
|
Path: svcCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: svcCfg.Defaults.APIPort},
|
Port: intstr.IntOrString{IntVal: svcCfg.Defaults.APIPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -195,7 +195,7 @@ func (r *CorePostgrestReconiler) reconilePostgrestDeployment(
|
||||||
SuccessThreshold: 2,
|
SuccessThreshold: 2,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/ready",
|
Path: serviceCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.AdminPort},
|
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.AdminPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -147,7 +147,7 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
||||||
SuccessThreshold: 2,
|
SuccessThreshold: 2,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/health",
|
Path: serviceCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -158,7 +158,7 @@ func (r *DashboardPGMetaReconciler) reconcilePGMetaDeployment(
|
||||||
TimeoutSeconds: 3,
|
TimeoutSeconds: 3,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/health",
|
Path: serviceCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -161,7 +161,7 @@ func (r *DashboardStudioReconciler) reconcileStudioDeployment(
|
||||||
SuccessThreshold: 2,
|
SuccessThreshold: 2,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/api/profile",
|
Path: serviceCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -172,7 +172,7 @@ func (r *DashboardStudioReconciler) reconcileStudioDeployment(
|
||||||
TimeoutSeconds: 3,
|
TimeoutSeconds: 3,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/api/profile",
|
Path: serviceCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.APIPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -225,7 +225,7 @@ func (r *StorageApiReconciler) reconcileStorageApiDeployment(
|
||||||
SuccessThreshold: 2,
|
SuccessThreshold: 2,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/status",
|
Path: serviceCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.ApiPort},
|
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.ApiPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -236,7 +236,7 @@ func (r *StorageApiReconciler) reconcileStorageApiDeployment(
|
||||||
TimeoutSeconds: 3,
|
TimeoutSeconds: 3,
|
||||||
ProbeHandler: corev1.ProbeHandler{
|
ProbeHandler: corev1.ProbeHandler{
|
||||||
HTTPGet: &corev1.HTTPGetAction{
|
HTTPGet: &corev1.HTTPGetAction{
|
||||||
Path: "/status",
|
Path: serviceCfg.LivenessProbePath,
|
||||||
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.ApiPort},
|
Port: intstr.IntOrString{IntVal: serviceCfg.Defaults.ApiPort},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -184,7 +184,7 @@ func (r *StorageImgProxyReconciler) reconcileImgProxyService(
|
||||||
var (
|
var (
|
||||||
serviceCfg = supabase.ServiceConfig.ImgProxy
|
serviceCfg = supabase.ServiceConfig.ImgProxy
|
||||||
imgProxyService = &corev1.Service{
|
imgProxyService = &corev1.Service{
|
||||||
ObjectMeta: supabase.ServiceConfig.Storage.ObjectMeta(storage),
|
ObjectMeta: supabase.ServiceConfig.ImgProxy.ObjectMeta(storage),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,9 @@ dynamic_resources:
|
||||||
|
|
||||||
static_resources:
|
static_resources:
|
||||||
clusters:
|
clusters:
|
||||||
- type: STRICT_DNS
|
- name: {{ .ControlPlane.Name }}
|
||||||
typed_extension_protocol_options:
|
type: STRICT_DNS
|
||||||
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
|
connect_timeout: 1s
|
||||||
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
|
|
||||||
explicit_http_config:
|
|
||||||
http2_protocol_options: {}
|
|
||||||
name: {{ .ControlPlane.Name }}
|
|
||||||
load_assignment:
|
load_assignment:
|
||||||
cluster_name: {{ .ControlPlane.Name }}
|
cluster_name: {{ .ControlPlane.Name }}
|
||||||
endpoints:
|
endpoints:
|
||||||
|
@ -31,9 +27,38 @@ static_resources:
|
||||||
socket_address:
|
socket_address:
|
||||||
address: {{ .ControlPlane.Host }}
|
address: {{ .ControlPlane.Host }}
|
||||||
port_value: {{ .ControlPlane.Port }}
|
port_value: {{ .ControlPlane.Port }}
|
||||||
|
typed_extension_protocol_options:
|
||||||
|
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
|
||||||
|
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
|
||||||
|
explicit_http_config:
|
||||||
|
http2_protocol_options: {}
|
||||||
|
transport_socket:
|
||||||
|
name: "envoy.transport_sockets.tls"
|
||||||
|
typed_config:
|
||||||
|
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext"
|
||||||
|
sni: {{ .ControlPlane.Host }}
|
||||||
|
common_tls_context:
|
||||||
|
tls_certificates:
|
||||||
|
- certificate_chain:
|
||||||
|
filename: /etc/envoy/certs/cp/tls.crt
|
||||||
|
private_key:
|
||||||
|
filename: /etc/envoy/certs/cp/tls.key
|
||||||
|
validation_context:
|
||||||
|
trusted_ca:
|
||||||
|
filename: /etc/envoy/certs/cp/ca.crt
|
||||||
|
|
||||||
|
|
||||||
admin:
|
admin:
|
||||||
address:
|
address:
|
||||||
socket_address:
|
socket_address:
|
||||||
address: 0.0.0.0
|
address: 0.0.0.0
|
||||||
port_value: 19000
|
port_value: 19000
|
||||||
|
|
||||||
|
application_log_config:
|
||||||
|
log_format:
|
||||||
|
json_format:
|
||||||
|
type: "app"
|
||||||
|
name: "%n"
|
||||||
|
timestamp: "%Y-%m-%dT%T.%F"
|
||||||
|
level: "%l"
|
||||||
|
message: "%j"
|
||||||
|
|
|
@ -19,14 +19,14 @@ package controlplane
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
|
cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
discoveryv1 "k8s.io/api/discovery/v1"
|
discoveryv1 "k8s.io/api/discovery/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
@ -46,6 +46,7 @@ import (
|
||||||
|
|
||||||
// APIGatewayReconciler reconciles a APIGateway object
|
// APIGatewayReconciler reconciles a APIGateway object
|
||||||
type APIGatewayReconciler struct {
|
type APIGatewayReconciler struct {
|
||||||
|
initialReconciliation atomic.Bool
|
||||||
client.Client
|
client.Client
|
||||||
Scheme *runtime.Scheme
|
Scheme *runtime.Scheme
|
||||||
Cache cachev3.SnapshotCache
|
Cache cachev3.SnapshotCache
|
||||||
|
@ -63,7 +64,7 @@ func (r *APIGatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||||
endpointSliceList discoveryv1.EndpointSliceList
|
endpointSliceList discoveryv1.EndpointSliceList
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.Info("Reconciling APIGateway")
|
logger.Info("Reconciling Envoy control-plane config")
|
||||||
|
|
||||||
if err := r.Get(ctx, req.NamespacedName, &gateway); client.IgnoreNotFound(err) != nil {
|
if err := r.Get(ctx, req.NamespacedName, &gateway); client.IgnoreNotFound(err) != nil {
|
||||||
logger.Error(err, "unable to fetch Gateway")
|
logger.Error(err, "unable to fetch Gateway")
|
||||||
|
@ -79,25 +80,31 @@ func (r *APIGatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||||
return ctrl.Result{}, err
|
return ctrl.Result{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
services := EnvoyServices{ServiceLabelKey: gateway.Spec.ComponentTypeLabel}
|
services := EnvoyServices{
|
||||||
|
ServiceLabelKey: gateway.Spec.ComponentTypeLabel,
|
||||||
|
Gateway: &gateway,
|
||||||
|
Client: r.Client,
|
||||||
|
}
|
||||||
services.UpsertEndpointSlices(endpointSliceList.Items...)
|
services.UpsertEndpointSlices(endpointSliceList.Items...)
|
||||||
|
|
||||||
rawServices, err := json.Marshal(services)
|
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, snapshotHash, err := services.snapshot(ctx, instance, gateway.Status.Envoy.ConfigVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctrl.Result{}, fmt.Errorf("failed to prepare config hash: %w", err)
|
return ctrl.Result{}, fmt.Errorf("failed to prepare snapshot: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceHash := fnv.New64a().Sum(rawServices)
|
if !r.initialReconciliation.CompareAndSwap(false, true) && bytes.Equal(gateway.Status.Envoy.ResourceHash, snapshotHash) {
|
||||||
if bytes.Equal(serviceHash, gateway.Status.Envoy.ResourceHash) {
|
logger.Info("No changes detected, skipping update")
|
||||||
logger.Info("Resource hash did not change - skipping reconciliation")
|
|
||||||
return ctrl.Result{}, nil
|
return ctrl.Result{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Updating service targets")
|
logger.Info("Updating service targets")
|
||||||
_, err = controllerutil.CreateOrPatch(ctx, r.Client, &gateway, func() error {
|
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, &gateway, func() error {
|
||||||
gateway.Status.ServiceTargets = services.Targets()
|
gateway.Status.ServiceTargets = services.Targets()
|
||||||
gateway.Status.Envoy.ConfigVersion = strconv.FormatInt(time.Now().UTC().UnixMilli(), 10)
|
gateway.Status.Envoy.ConfigVersion = strconv.FormatInt(time.Now().UTC().UnixMilli(), 10)
|
||||||
gateway.Status.Envoy.ResourceHash = serviceHash
|
gateway.Status.Envoy.ResourceHash = snapshotHash
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
@ -105,14 +112,6 @@ func (r *APIGatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||||
return ctrl.Result{}, err
|
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)
|
logger.Info("Propagating Envoy snapshot", "version", gateway.Status.Envoy.ConfigVersion)
|
||||||
if err := r.Cache.SetSnapshot(ctx, instance, snapshot); err != nil {
|
if err := r.Cache.SetSnapshot(ctx, instance, snapshot); err != nil {
|
||||||
return ctrl.Result{}, fmt.Errorf("failed to propagate snapshot: %w", err)
|
return ctrl.Result{}, fmt.Errorf("failed to propagate snapshot: %w", err)
|
||||||
|
@ -133,7 +132,8 @@ func (r *APIGatewayReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(new(supabasev1alpha1.APIGateway)).
|
For(new(supabasev1alpha1.APIGateway), builder.WithPredicates(predicate.GenerationChangedPredicate{})).
|
||||||
|
Owns(new(corev1.Secret), builder.WithPredicates(predicate.GenerationChangedPredicate{})).
|
||||||
Watches(
|
Watches(
|
||||||
new(discoveryv1.EndpointSlice),
|
new(discoveryv1.EndpointSlice),
|
||||||
r.endpointSliceEventHandler(),
|
r.endpointSliceEventHandler(),
|
||||||
|
@ -156,6 +156,7 @@ func (r *APIGatewayReconciler) endpointSliceEventHandler() handler.TypedEventHan
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Info("Triggering APIGateway reconciliation", "obj_name", obj.GetName(), "obj_namespace", obj.GetNamespace())
|
||||||
if err := r.Client.List(ctx, &apiGatewayList, client.InNamespace(endpointSlice.Namespace)); err != 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")
|
logger.Error(err, "failed to list APIGateways to determine reconcile targets")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -17,9 +17,7 @@ limitations under the License.
|
||||||
package controlplane
|
package controlplane
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -30,27 +28,10 @@ import (
|
||||||
discoveryv1 "k8s.io/api/discovery/v1"
|
discoveryv1 "k8s.io/api/discovery/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ json.Marshaler = (*ServiceCluster)(nil)
|
|
||||||
|
|
||||||
type ServiceCluster struct {
|
type ServiceCluster struct {
|
||||||
ServiceEndpoints map[string]Endpoints
|
ServiceEndpoints map[string]Endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func (c *ServiceCluster) AddOrUpdateEndpoints(eps discoveryv1.EndpointSlice) {
|
||||||
if c.ServiceEndpoints == nil {
|
if c.ServiceEndpoints == nil {
|
||||||
c.ServiceEndpoints = make(map[string]Endpoints)
|
c.ServiceEndpoints = make(map[string]Endpoints)
|
||||||
|
|
|
@ -22,4 +22,6 @@ const (
|
||||||
FilterNameCORS = "envoy.filters.http.cors"
|
FilterNameCORS = "envoy.filters.http.cors"
|
||||||
FilterNameHttpRouter = "envoy.filters.http.router"
|
FilterNameHttpRouter = "envoy.filters.http.router"
|
||||||
FilterNameHttpConnectionManager = "envoy.filters.network.http_connection_manager"
|
FilterNameHttpConnectionManager = "envoy.filters.network.http_connection_manager"
|
||||||
|
FilterNameBasicAuth = "envoy.filters.http.basic_auth"
|
||||||
|
FilterNameOAuth2 = "envoy.filters.http.oauth2"
|
||||||
)
|
)
|
||||||
|
|
62
internal/controlplane/logging.go
Normal file
62
internal/controlplane/logging.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
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 (
|
||||||
|
accesslogv3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
|
||||||
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
streamv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/stream/v3"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AccessLog(name string) *accesslogv3.AccessLog {
|
||||||
|
return &accesslogv3.AccessLog{
|
||||||
|
Name: name,
|
||||||
|
Filter: &accesslogv3.AccessLogFilter{
|
||||||
|
FilterSpecifier: &accesslogv3.AccessLogFilter_NotHealthCheckFilter{
|
||||||
|
NotHealthCheckFilter: new(accesslogv3.NotHealthCheckFilter),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ConfigType: &accesslogv3.AccessLog_TypedConfig{
|
||||||
|
TypedConfig: MustAny(&streamv3.StderrAccessLog{
|
||||||
|
AccessLogFormat: &streamv3.StderrAccessLog_LogFormat{
|
||||||
|
LogFormat: &corev3.SubstitutionFormatString{
|
||||||
|
Format: &corev3.SubstitutionFormatString_JsonFormat{
|
||||||
|
JsonFormat: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{
|
||||||
|
"type": {Kind: &structpb.Value_StringValue{StringValue: "access_log"}},
|
||||||
|
"timestamp": {Kind: &structpb.Value_StringValue{StringValue: "%START_TIME%"}},
|
||||||
|
"protocol": {Kind: &structpb.Value_StringValue{StringValue: "%PROTOCOL%"}},
|
||||||
|
"http_method": {Kind: &structpb.Value_StringValue{StringValue: "%REQ(:METHOD)%"}},
|
||||||
|
"original_path": {Kind: &structpb.Value_StringValue{StringValue: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"}},
|
||||||
|
"forwarded_for": {Kind: &structpb.Value_StringValue{StringValue: "%REQ(X-FORWARDED-FOR)%"}},
|
||||||
|
"user_agent": {Kind: &structpb.Value_StringValue{StringValue: "%REQ(USER-AGENT)%"}},
|
||||||
|
"request_id": {Kind: &structpb.Value_StringValue{StringValue: "%REQ(X-REQUEST-ID)%"}},
|
||||||
|
"authority": {Kind: &structpb.Value_StringValue{StringValue: "%REQ(:AUTHORITY)%"}},
|
||||||
|
"upstream_host": {Kind: &structpb.Value_StringValue{StringValue: "%UPSTREAM_HOST%"}},
|
||||||
|
"response_code": {Kind: &structpb.Value_StringValue{StringValue: "%RESPONSE_CODE%"}},
|
||||||
|
"response_code_details": {Kind: &structpb.Value_StringValue{StringValue: "%RESPONSE_CODE_DETAILS%"}},
|
||||||
|
"trace_id": {Kind: &structpb.Value_StringValue{StringValue: "%TRACE_ID%"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,30 +18,56 @@ package controlplane
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
accesslogv3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
|
||||||
|
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||||
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
||||||
|
endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
||||||
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||||
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
router "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
|
oauth2v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/oauth2/v3"
|
||||||
|
routerv3 "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"
|
hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
|
||||||
|
tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
||||||
|
matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||||
"github.com/envoyproxy/go-control-plane/pkg/cache/types"
|
"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/cache/v3"
|
||||||
"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
|
"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
discoveryv1 "k8s.io/api/discovery/v1"
|
discoveryv1 "k8s.io/api/discovery/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
|
||||||
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
studioRouteName = "supabase-studio"
|
||||||
|
dashboardOAuth2ClusterName = "dashboard-oauth"
|
||||||
|
apiRouteName = "supabase-api"
|
||||||
|
apilistenerName = "supabase-api"
|
||||||
|
)
|
||||||
|
|
||||||
type EnvoyServices struct {
|
type EnvoyServices struct {
|
||||||
ServiceLabelKey string `json:"-"`
|
client.Client
|
||||||
Postgrest *PostgrestCluster `json:"postgrest,omitempty"`
|
ServiceLabelKey string
|
||||||
GoTrue *GoTrueCluster `json:"auth,omitempty"`
|
Gateway *supabasev1alpha1.APIGateway
|
||||||
StorageApi *StorageApiCluster `json:"storageApi,omitempty"`
|
Postgrest *PostgrestCluster
|
||||||
PGMeta *PGMetaCluster `json:"pgmeta,omitempty"`
|
GoTrue *GoTrueCluster
|
||||||
Studio *StudioCluster `json:"studio,omitempty"`
|
StorageApi *StorageApiCluster
|
||||||
|
PGMeta *PGMetaCluster
|
||||||
|
Studio *StudioCluster
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EnvoyServices) UpsertEndpointSlices(endpointSlices ...discoveryv1.EndpointSlice) {
|
func (s *EnvoyServices) UpsertEndpointSlices(endpointSlices ...discoveryv1.EndpointSlice) {
|
||||||
|
@ -67,6 +93,12 @@ func (s *EnvoyServices) UpsertEndpointSlices(endpointSlices ...discoveryv1.Endpo
|
||||||
s.PGMeta = new(PGMetaCluster)
|
s.PGMeta = new(PGMetaCluster)
|
||||||
}
|
}
|
||||||
s.PGMeta.AddOrUpdateEndpoints(eps)
|
s.PGMeta.AddOrUpdateEndpoints(eps)
|
||||||
|
case supabase.ServiceConfig.Studio.Name:
|
||||||
|
if s.Studio == nil {
|
||||||
|
s.Studio = new(StudioCluster)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Studio.AddOrUpdateEndpoints(eps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,19 +129,173 @@ func (s EnvoyServices) Targets() map[string][]string {
|
||||||
return targets
|
return targets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *EnvoyServices) snapshot(ctx context.Context, instance, version string) (*cache.Snapshot, error) {
|
func (s *EnvoyServices) snapshot(ctx context.Context, instance, version string) (snapshot *cache.Snapshot, snapshotHash []byte, err error) {
|
||||||
const (
|
|
||||||
apiRouteName = "supabase"
|
|
||||||
studioRouteName = "supabas-studio"
|
|
||||||
vHostName = "supabase"
|
|
||||||
listenerName = "supabase"
|
|
||||||
)
|
|
||||||
|
|
||||||
logger := log.FromContext(ctx)
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
apiConnectionManager := &hcm.HttpConnectionManager{
|
listeners := []*listenerv3.Listener{{
|
||||||
|
Name: apilistenerName,
|
||||||
|
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(s.apiConnectionManager()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
if studioListener := s.studioListener(); studioListener != nil {
|
||||||
|
logger.Info("Adding studio listener")
|
||||||
|
listeners = append(listeners, studioListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := []types.Resource{s.apiRouteConfiguration(instance)}
|
||||||
|
|
||||||
|
if studioRouteCfg := s.studioRoute(instance); studioRouteCfg != nil {
|
||||||
|
logger.Info("Adding studio route")
|
||||||
|
routes = append(routes, studioRouteCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
clusters := castResources(
|
||||||
|
slices.Concat(
|
||||||
|
s.Postgrest.Cluster(instance),
|
||||||
|
s.GoTrue.Cluster(instance),
|
||||||
|
s.StorageApi.Cluster(instance),
|
||||||
|
s.PGMeta.Cluster(instance),
|
||||||
|
s.Studio.Cluster(instance),
|
||||||
|
)...)
|
||||||
|
|
||||||
|
if oauth2Spec := s.Gateway.Spec.DashboardEndpoint.OAuth2(); oauth2Spec != nil {
|
||||||
|
if oauth2TokenEndpointCluster, err := s.oauth2TokenEndpointCluster(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else {
|
||||||
|
logger.Info("Adding OAuth2 token endpoint cluster")
|
||||||
|
clusters = append(clusters, oauth2TokenEndpointCluster)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sdsSecrets, err := s.secrets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to collect dynamic secrets: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawSnapshot := map[resource.Type][]types.Resource{
|
||||||
|
resource.ClusterType: clusters,
|
||||||
|
resource.RouteType: routes,
|
||||||
|
resource.ListenerType: castResources(listeners...),
|
||||||
|
resource.SecretType: sdsSecrets,
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
|
||||||
|
for _, resType := range []resource.Type{resource.ClusterType, resource.RouteType, resource.ListenerType, resource.SecretType} {
|
||||||
|
for _, resource := range rawSnapshot[resType] {
|
||||||
|
if raw, err := proto.Marshal(resource); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
} else {
|
||||||
|
_, _ = hash.Write(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot, err = cache.NewSnapshot(
|
||||||
|
version,
|
||||||
|
rawSnapshot,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := snapshot.Consistent(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot, hash.Sum(nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EnvoyServices) secrets(ctx context.Context) ([]types.Resource, error) {
|
||||||
|
var (
|
||||||
|
serviceConfig = supabase.ServiceConfig.Envoy
|
||||||
|
resources = make([]types.Resource, 0, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
hmacSecret, err := s.k8sSecretKeyToSecret(
|
||||||
|
ctx,
|
||||||
|
serviceConfig.HmacSecretName(s.Gateway),
|
||||||
|
serviceConfig.Defaults.HmacSecretKey,
|
||||||
|
serviceConfig.Defaults.HmacSecretKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
resources = append(resources, hmacSecret)
|
||||||
|
} else if client.IgnoreNotFound(err) != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if oauth2Spec := s.Gateway.Spec.DashboardEndpoint.OAuth2(); oauth2Spec != nil {
|
||||||
|
oauth2ClientSecret, err := s.k8sSecretKeyToSecret(
|
||||||
|
ctx,
|
||||||
|
oauth2Spec.ClientSecretRef.Name,
|
||||||
|
oauth2Spec.ClientSecretRef.Key,
|
||||||
|
serviceConfig.Defaults.OAuth2ClientSecretKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
resources = append(resources, oauth2ClientSecret)
|
||||||
|
} else if client.IgnoreNotFound(err) != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EnvoyServices) k8sSecretKeyToSecret(ctx context.Context, secretName, secretKey, envoySecretName string) (*tlsv3.Secret, error) {
|
||||||
|
k8sSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: secretName,
|
||||||
|
Namespace: s.Gateway.Namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Get(ctx, client.ObjectKeyFromObject(k8sSecret), k8sSecret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tlsv3.Secret{
|
||||||
|
Name: envoySecretName,
|
||||||
|
Type: &tlsv3.Secret_GenericSecret{
|
||||||
|
GenericSecret: &tlsv3.GenericSecret{
|
||||||
|
Secret: &corev3.DataSource{
|
||||||
|
Specifier: &corev3.DataSource_InlineBytes{
|
||||||
|
InlineBytes: k8sSecret.Data[secretKey],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EnvoyServices) apiConnectionManager() *hcm.HttpConnectionManager {
|
||||||
|
return &hcm.HttpConnectionManager{
|
||||||
CodecType: hcm.HttpConnectionManager_AUTO,
|
CodecType: hcm.HttpConnectionManager_AUTO,
|
||||||
StatPrefix: "http",
|
StatPrefix: "supabase_rest",
|
||||||
|
AccessLog: []*accesslogv3.AccessLog{AccessLog("supabase-rest-access-log")},
|
||||||
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
||||||
Rds: &hcm.Rds{
|
Rds: &hcm.Rds{
|
||||||
ConfigSource: &corev3.ConfigSource{
|
ConfigSource: &corev3.ConfigSource{
|
||||||
|
@ -141,14 +327,109 @@ func (s *EnvoyServices) snapshot(ctx context.Context, instance, version string)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: FilterNameHttpRouter,
|
Name: FilterNameHttpRouter,
|
||||||
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(new(router.Router))},
|
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(new(routerv3.Router))},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EnvoyServices) apiRouteConfiguration(instance string) *routev3.RouteConfiguration {
|
||||||
|
return &routev3.RouteConfiguration{
|
||||||
|
Name: apiRouteName,
|
||||||
|
VirtualHosts: []*routev3.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.StorageApi.Routes(instance),
|
||||||
|
s.PGMeta.Routes(instance),
|
||||||
|
),
|
||||||
|
}},
|
||||||
|
TypedPerFilterConfig: map[string]*anypb.Any{
|
||||||
|
FilterNameCORS: MustAny(CorsPolicy()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EnvoyServices) studioListener() *listenerv3.Listener {
|
||||||
|
if s.Studio == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
httpFilters []*hcm.HttpFilter
|
||||||
|
serviceCfg = supabase.ServiceConfig.Envoy
|
||||||
|
)
|
||||||
|
|
||||||
|
if oauth2Spec := s.Gateway.Spec.DashboardEndpoint.OAuth2(); oauth2Spec != nil {
|
||||||
|
httpFilters = append(httpFilters, &hcm.HttpFilter{
|
||||||
|
Name: FilterNameOAuth2,
|
||||||
|
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(&oauth2v3.OAuth2{
|
||||||
|
Config: &oauth2v3.OAuth2Config{
|
||||||
|
TokenEndpoint: &corev3.HttpUri{
|
||||||
|
HttpUpstreamType: &corev3.HttpUri_Cluster{
|
||||||
|
Cluster: dashboardOAuth2ClusterName,
|
||||||
|
},
|
||||||
|
Uri: s.Gateway.Spec.DashboardEndpoint.Auth.OAuth2.TokenEndpoint,
|
||||||
|
Timeout: durationpb.New(3 * time.Second),
|
||||||
|
},
|
||||||
|
AuthorizationEndpoint: s.Gateway.Spec.DashboardEndpoint.Auth.OAuth2.AuthorizationEndpoint,
|
||||||
|
RedirectUri: "%REQ(x-forwarded-proto)%://%REQ(:authority)%/callback",
|
||||||
|
RedirectPathMatcher: &matcherv3.PathMatcher{
|
||||||
|
Rule: &matcherv3.PathMatcher_Path{
|
||||||
|
Path: &matcherv3.StringMatcher{
|
||||||
|
MatchPattern: &matcherv3.StringMatcher_Exact{
|
||||||
|
Exact: "/callback",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SignoutPath: &matcherv3.PathMatcher{
|
||||||
|
Rule: &matcherv3.PathMatcher_Path{
|
||||||
|
Path: &matcherv3.StringMatcher{
|
||||||
|
MatchPattern: &matcherv3.StringMatcher_Exact{
|
||||||
|
Exact: "/signout",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Credentials: &oauth2v3.OAuth2Credentials{
|
||||||
|
ClientId: oauth2Spec.ClientID,
|
||||||
|
TokenSecret: &tlsv3.SdsSecretConfig{
|
||||||
|
Name: serviceCfg.Defaults.OAuth2ClientSecretKey,
|
||||||
|
SdsConfig: &corev3.ConfigSource{
|
||||||
|
ConfigSourceSpecifier: &corev3.ConfigSource_Ads{
|
||||||
|
Ads: new(corev3.AggregatedConfigSource),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TokenFormation: &oauth2v3.OAuth2Credentials_HmacSecret{
|
||||||
|
HmacSecret: &tlsv3.SdsSecretConfig{
|
||||||
|
Name: serviceCfg.Defaults.HmacSecretKey,
|
||||||
|
SdsConfig: &corev3.ConfigSource{
|
||||||
|
ConfigSourceSpecifier: &corev3.ConfigSource_Ads{
|
||||||
|
Ads: new(corev3.AggregatedConfigSource),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuthScopes: oauth2Spec.Scopes,
|
||||||
|
Resources: oauth2Spec.Resources,
|
||||||
|
},
|
||||||
|
})},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
studioConnetionManager := &hcm.HttpConnectionManager{
|
studioConnetionManager := &hcm.HttpConnectionManager{
|
||||||
CodecType: hcm.HttpConnectionManager_AUTO,
|
CodecType: hcm.HttpConnectionManager_AUTO,
|
||||||
StatPrefix: "http",
|
StatPrefix: "supbase_studio",
|
||||||
|
AccessLog: []*accesslogv3.AccessLog{AccessLog("supbase_studio_access_log")},
|
||||||
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
|
||||||
Rds: &hcm.Rds{
|
Rds: &hcm.Rds{
|
||||||
ConfigSource: &corev3.ConfigSource{
|
ConfigSource: &corev3.ConfigSource{
|
||||||
|
@ -169,68 +450,13 @@ func (s *EnvoyServices) snapshot(ctx context.Context, instance, version string)
|
||||||
RouteConfigName: studioRouteName,
|
RouteConfigName: studioRouteName,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HttpFilters: []*hcm.HttpFilter{
|
HttpFilters: append(httpFilters, &hcm.HttpFilter{
|
||||||
{
|
|
||||||
Name: FilterNameHttpRouter,
|
Name: FilterNameHttpRouter,
|
||||||
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(new(router.Router))},
|
ConfigType: &hcm.HttpFilter_TypedConfig{TypedConfig: MustAny(new(routerv3.Router))},
|
||||||
},
|
}),
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiRouteCfg := &route.RouteConfiguration{
|
return &listenerv3.Listener{
|
||||||
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.StorageApi.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",
|
Name: "studio",
|
||||||
Address: &corev3.Address{
|
Address: &corev3.Address{
|
||||||
Address: &corev3.Address_SocketAddress{
|
Address: &corev3.Address_SocketAddress{
|
||||||
|
@ -255,34 +481,106 @@ func (s *EnvoyServices) snapshot(ctx context.Context, instance, version string)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rawSnapshot := map[resource.Type][]types.Resource{
|
func (s *EnvoyServices) studioRoute(instance string) *routev3.RouteConfiguration {
|
||||||
resource.ClusterType: castResources(
|
if s.Studio == nil {
|
||||||
slices.Concat(
|
return nil
|
||||||
s.Postgrest.Cluster(instance),
|
|
||||||
s.GoTrue.Cluster(instance),
|
|
||||||
s.StorageApi.Cluster(instance),
|
|
||||||
s.PGMeta.Cluster(instance),
|
|
||||||
)...),
|
|
||||||
resource.RouteType: {apiRouteCfg},
|
|
||||||
resource.ListenerType: castResources(listeners...),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot, err := cache.NewSnapshot(
|
return &routev3.RouteConfiguration{
|
||||||
version,
|
Name: studioRouteName,
|
||||||
rawSnapshot,
|
VirtualHosts: []*routev3.VirtualHost{{
|
||||||
)
|
Name: "supabase-studio",
|
||||||
|
Domains: []string{"*"},
|
||||||
|
Routes: s.Studio.Routes(instance),
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EnvoyServices) oauth2TokenEndpointCluster() (*clusterv3.Cluster, error) {
|
||||||
|
oauth2Spec := s.Gateway.Spec.DashboardEndpoint.OAuth2()
|
||||||
|
parsedTokenEndpoint, err := url.Parse(oauth2Spec.TokenEndpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to parse token endpoint: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := snapshot.Consistent(); err != nil {
|
var (
|
||||||
return nil, err
|
endpointPort uint32
|
||||||
|
tls bool
|
||||||
|
)
|
||||||
|
switch parsedTokenEndpoint.Scheme {
|
||||||
|
case "http":
|
||||||
|
endpointPort = 80
|
||||||
|
case "https":
|
||||||
|
endpointPort = 443
|
||||||
|
tls = true
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported token endpoint scheme: %s", parsedTokenEndpoint.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshot, nil
|
if tokenEndpointPort := parsedTokenEndpoint.Port(); tokenEndpointPort != "" {
|
||||||
|
if parsedPort, err := strconv.ParseUint(tokenEndpointPort, 10, 32); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse token endpoint port: %w", err)
|
||||||
|
} else {
|
||||||
|
endpointPort = uint32(parsedPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster := &clusterv3.Cluster{
|
||||||
|
Name: dashboardOAuth2ClusterName,
|
||||||
|
ConnectTimeout: durationpb.New(3 * time.Second),
|
||||||
|
ClusterDiscoveryType: &clusterv3.Cluster_Type{
|
||||||
|
Type: clusterv3.Cluster_LOGICAL_DNS,
|
||||||
|
},
|
||||||
|
LbPolicy: clusterv3.Cluster_ROUND_ROBIN,
|
||||||
|
LoadAssignment: &endpointv3.ClusterLoadAssignment{
|
||||||
|
ClusterName: dashboardOAuth2ClusterName,
|
||||||
|
Endpoints: []*endpointv3.LocalityLbEndpoints{{
|
||||||
|
LbEndpoints: []*endpointv3.LbEndpoint{{
|
||||||
|
HostIdentifier: &endpointv3.LbEndpoint_Endpoint{
|
||||||
|
Endpoint: &endpointv3.Endpoint{
|
||||||
|
Address: &corev3.Address{
|
||||||
|
Address: &corev3.Address_SocketAddress{
|
||||||
|
SocketAddress: &corev3.SocketAddress{
|
||||||
|
Address: parsedTokenEndpoint.Hostname(),
|
||||||
|
PortSpecifier: &corev3.SocketAddress_PortValue{
|
||||||
|
PortValue: endpointPort,
|
||||||
|
},
|
||||||
|
Protocol: corev3.SocketAddress_TCP,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Gateway.Spec.Envoy != nil && s.Gateway.Spec.Envoy.DisableIPv6 {
|
||||||
|
cluster.DnsLookupFamily = clusterv3.Cluster_V4_ONLY
|
||||||
|
}
|
||||||
|
|
||||||
|
if tls {
|
||||||
|
cluster.TransportSocket = &corev3.TransportSocket{
|
||||||
|
Name: "envoy.transport_sockets.tls",
|
||||||
|
ConfigType: &corev3.TransportSocket_TypedConfig{
|
||||||
|
TypedConfig: MustAny(&tlsv3.UpstreamTlsContext{
|
||||||
|
Sni: parsedTokenEndpoint.Hostname(),
|
||||||
|
AllowRenegotiation: true,
|
||||||
|
CommonTlsContext: &tlsv3.CommonTlsContext{
|
||||||
|
TlsParams: &tlsv3.TlsParameters{
|
||||||
|
TlsMinimumProtocolVersion: tlsv3.TlsParameters_TLSv1_2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cluster, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func castResources[T types.Resource](from ...T) []types.Resource {
|
func castResources[T types.Resource](from ...T) []types.Resource {
|
||||||
|
|
|
@ -16,6 +16,50 @@ limitations under the License.
|
||||||
|
|
||||||
package controlplane
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||||
|
routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||||
|
|
||||||
|
"code.icb4dc0.de/prskr/supabase-operator/internal/supabase"
|
||||||
|
)
|
||||||
|
|
||||||
type StudioCluster struct {
|
type StudioCluster struct {
|
||||||
ServiceCluster
|
ServiceCluster
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *StudioCluster) Cluster(instance string) []*clusterv3.Cluster {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceCfg := supabase.ServiceConfig.Studio
|
||||||
|
|
||||||
|
return []*clusterv3.Cluster{
|
||||||
|
c.ServiceCluster.Cluster(fmt.Sprintf("%s@%s", serviceCfg.Name, instance), uint32(serviceCfg.Defaults.APIPort)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StudioCluster) Routes(instance string) []*routev3.Route {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*routev3.Route{{
|
||||||
|
Name: "Studio: /* -> http://studio:3000/*",
|
||||||
|
Match: &routev3.RouteMatch{
|
||||||
|
PathSpecifier: &routev3.RouteMatch_Prefix{
|
||||||
|
Prefix: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: &routev3.Route_Route{
|
||||||
|
Route: &routev3.RouteAction{
|
||||||
|
ClusterSpecifier: &routev3.RouteAction_Cluster{
|
||||||
|
Cluster: fmt.Sprintf("%s@%s", supabase.ServiceConfig.Studio.Name, instance),
|
||||||
|
},
|
||||||
|
PrefixRewrite: "/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
42
internal/health/cert_valid.go
Normal file
42
internal/health/cert_valid.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
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 health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrCertExpired = errors.New("certificate expired")
|
||||||
|
|
||||||
|
func CertValidCheck(cert tls.Certificate) healthz.Checker {
|
||||||
|
return func(req *http.Request) error {
|
||||||
|
if cert.Leaf.NotAfter.Before(time.Now()) {
|
||||||
|
err := fmt.Errorf("%w: %s", ErrCertExpired, cert.Leaf.Subject.CommonName)
|
||||||
|
ctrl.Log.Error(err, "certificate expired", "commonName", cert.Leaf.Subject.CommonName, "not_after", cert.Leaf.NotAfter)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ type authConfigDefaults struct {
|
||||||
func authServiceConfig() serviceConfig[authEnvKeys, authConfigDefaults] {
|
func authServiceConfig() serviceConfig[authEnvKeys, authConfigDefaults] {
|
||||||
return serviceConfig[authEnvKeys, authConfigDefaults]{
|
return serviceConfig[authEnvKeys, authConfigDefaults]{
|
||||||
Name: "auth",
|
Name: "auth",
|
||||||
|
LivenessProbePath: "/health",
|
||||||
EnvKeys: authEnvKeys{
|
EnvKeys: authEnvKeys{
|
||||||
ApiHost: fixedEnvOf("GOTRUE_API_HOST", "0.0.0.0"),
|
ApiHost: fixedEnvOf("GOTRUE_API_HOST", "0.0.0.0"),
|
||||||
ApiPort: fixedEnvOf("GOTRUE_API_PORT", "9999"),
|
ApiPort: fixedEnvOf("GOTRUE_API_PORT", "9999"),
|
||||||
|
|
|
@ -24,10 +24,23 @@ import (
|
||||||
|
|
||||||
type serviceConfig[TEnvKeys, TDefaults any] struct {
|
type serviceConfig[TEnvKeys, TDefaults any] struct {
|
||||||
Name string
|
Name string
|
||||||
|
LivenessProbePath string
|
||||||
|
ReadinessProbePath string
|
||||||
EnvKeys TEnvKeys
|
EnvKeys TEnvKeys
|
||||||
Defaults TDefaults
|
Defaults TDefaults
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg serviceConfig[TEnvKeys, TDefaults]) ReadinessPath() string {
|
||||||
|
return cfg.ReadinessProbePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg serviceConfig[TEnvKeys, TDefaults]) LivenessPath() string {
|
||||||
|
if cfg.LivenessProbePath == "" {
|
||||||
|
return cfg.ReadinessProbePath
|
||||||
|
}
|
||||||
|
return cfg.LivenessProbePath
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg serviceConfig[TEnvKeys, TDefaults]) ObjectName(obj metav1.Object) string {
|
func (cfg serviceConfig[TEnvKeys, TDefaults]) ObjectName(obj metav1.Object) string {
|
||||||
return fmt.Sprintf("%s-%s", obj.GetName(), cfg.Name)
|
return fmt.Sprintf("%s-%s", obj.GetName(), cfg.Name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,15 +26,26 @@ func newEnvoyServiceConfig() envoyServiceConfig {
|
||||||
return envoyServiceConfig{
|
return envoyServiceConfig{
|
||||||
Defaults: envoyDefaults{
|
Defaults: envoyDefaults{
|
||||||
ConfigKey: "config.yaml",
|
ConfigKey: "config.yaml",
|
||||||
|
OAuth2ClientSecretKey: "oauth2_client_secret",
|
||||||
|
HmacSecretKey: "oauth2_hmac_secret",
|
||||||
UID: 65532,
|
UID: 65532,
|
||||||
GID: 65532,
|
GID: 65532,
|
||||||
|
StudioPortName: "studio",
|
||||||
|
ApiPortName: "api",
|
||||||
|
StudioPort: 3000,
|
||||||
|
ApiPort: 8000,
|
||||||
|
AdminPort: 19000,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type envoyDefaults struct {
|
type envoyDefaults struct {
|
||||||
ConfigKey string
|
ConfigKey string
|
||||||
|
HmacSecretKey string
|
||||||
|
OAuth2ClientSecretKey string
|
||||||
UID, GID int64
|
UID, GID int64
|
||||||
|
StudioPortName, ApiPortName string
|
||||||
|
StudioPort, ApiPort, AdminPort int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type envoyServiceConfig struct {
|
type envoyServiceConfig struct {
|
||||||
|
@ -44,3 +55,11 @@ type envoyServiceConfig struct {
|
||||||
func (envoyServiceConfig) ObjectName(obj metav1.Object) string {
|
func (envoyServiceConfig) ObjectName(obj metav1.Object) string {
|
||||||
return fmt.Sprintf("%s-envoy", obj.GetName())
|
return fmt.Sprintf("%s-envoy", obj.GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (envoyServiceConfig) ControlPlaneClientCertSecretName(obj metav1.Object) string {
|
||||||
|
return fmt.Sprintf("%s-cp-client-cert", obj.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (envoyServiceConfig) HmacSecretName(obj metav1.Object) string {
|
||||||
|
return fmt.Sprintf("%s-hmac-secret", obj.GetName())
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ type pgMetaDefaults struct {
|
||||||
func pgMetaServiceConfig() serviceConfig[pgMetaEnvKeys, pgMetaDefaults] {
|
func pgMetaServiceConfig() serviceConfig[pgMetaEnvKeys, pgMetaDefaults] {
|
||||||
return serviceConfig[pgMetaEnvKeys, pgMetaDefaults]{
|
return serviceConfig[pgMetaEnvKeys, pgMetaDefaults]{
|
||||||
Name: "pg-meta",
|
Name: "pg-meta",
|
||||||
|
LivenessProbePath: "/health",
|
||||||
EnvKeys: pgMetaEnvKeys{
|
EnvKeys: pgMetaEnvKeys{
|
||||||
APIPort: "PG_META_PORT",
|
APIPort: "PG_META_PORT",
|
||||||
DBHost: "PG_META_DB_HOST",
|
DBHost: "PG_META_DB_HOST",
|
||||||
|
|
|
@ -43,6 +43,7 @@ type postgrestConfigDefaults struct {
|
||||||
func postgrestServiceConfig() serviceConfig[postgrestEnvKeys, postgrestConfigDefaults] {
|
func postgrestServiceConfig() serviceConfig[postgrestEnvKeys, postgrestConfigDefaults] {
|
||||||
return serviceConfig[postgrestEnvKeys, postgrestConfigDefaults]{
|
return serviceConfig[postgrestEnvKeys, postgrestConfigDefaults]{
|
||||||
Name: "postgrest",
|
Name: "postgrest",
|
||||||
|
LivenessProbePath: "/ready",
|
||||||
EnvKeys: postgrestEnvKeys{
|
EnvKeys: postgrestEnvKeys{
|
||||||
Host: fixedEnvOf("PGRST_SERVER_HOST", "*"),
|
Host: fixedEnvOf("PGRST_SERVER_HOST", "*"),
|
||||||
DBUri: "PGRST_DB_URI",
|
DBUri: "PGRST_DB_URI",
|
||||||
|
|
|
@ -54,6 +54,7 @@ type storageApiDefaults struct {
|
||||||
func storageServiceConfig() serviceConfig[storageEnvApiKeys, storageApiDefaults] {
|
func storageServiceConfig() serviceConfig[storageEnvApiKeys, storageApiDefaults] {
|
||||||
return serviceConfig[storageEnvApiKeys, storageApiDefaults]{
|
return serviceConfig[storageEnvApiKeys, storageApiDefaults]{
|
||||||
Name: "storage-api",
|
Name: "storage-api",
|
||||||
|
LivenessProbePath: "/status",
|
||||||
EnvKeys: storageEnvApiKeys{
|
EnvKeys: storageEnvApiKeys{
|
||||||
AnonKey: "ANON_KEY",
|
AnonKey: "ANON_KEY",
|
||||||
ServiceKey: "SERVICE_KEY",
|
ServiceKey: "SERVICE_KEY",
|
||||||
|
|
|
@ -37,6 +37,7 @@ type studioDefaults struct {
|
||||||
func studioServiceConfig() serviceConfig[studioEnvKeys, studioDefaults] {
|
func studioServiceConfig() serviceConfig[studioEnvKeys, studioDefaults] {
|
||||||
return serviceConfig[studioEnvKeys, studioDefaults]{
|
return serviceConfig[studioEnvKeys, studioDefaults]{
|
||||||
Name: "studio",
|
Name: "studio",
|
||||||
|
LivenessProbePath: "/api/profile",
|
||||||
EnvKeys: studioEnvKeys{
|
EnvKeys: studioEnvKeys{
|
||||||
PGMetaURL: "STUDIO_PG_META_URL",
|
PGMetaURL: "STUDIO_PG_META_URL",
|
||||||
DBPassword: "POSTGRES_PASSWORD",
|
DBPassword: "POSTGRES_PASSWORD",
|
||||||
|
|
Loading…
Add table
Reference in a new issue