diff --git a/api/v1alpha1/apigateway_types.go b/api/v1alpha1/apigateway_types.go index e246c33..4c03dbb 100644 --- a/api/v1alpha1/apigateway_types.go +++ b/api/v1alpha1/apigateway_types.go @@ -48,12 +48,23 @@ type EnvoySpec struct { WorkloadTemplate *WorkloadTemplate `json:"workloadTemplate,omitempty"` } +type ApiEndpointSpec struct { + // JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs + JWKSSelector *corev1.SecretKeySelector `json:"jwks"` +} + +type DashboardEndpointSpec struct{} + // APIGatewaySpec defines the desired state of APIGateway. type APIGatewaySpec struct { // Envoy - configure the envoy instance and most importantly the control-plane Envoy *EnvoySpec `json:"envoy"` - // JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs - JWKSSelector *corev1.SecretKeySelector `json:"jwks"` + // ApiEndpoint - Configure the endpoint for all API routes + // this includes the JWT configuration + ApiEndpoint *ApiEndpointSpec `json:"apiEndpoint,omitempty"` + // DashboardEndpoint - Configure the endpoint for the Supabase dashboard (studio) + // this includes optional authentication (basic or Oauth2) for the dashboard + DashboardEndpoint *DashboardEndpointSpec `json:"dashboardEndpoint,omitempty"` // ServiceSelector - selector to match all Supabase services (or in fact EndpointSlices) that should be considered for this APIGateway // +kubebuilder:default={"matchExpressions":{{"key": "app.kubernetes.io/part-of", "operator":"In", "values":{"supabase"}},{"key":"supabase.k8s.icb4dc0.de/api-gateway-target","operator":"Exists"}}} ServiceSelector *metav1.LabelSelector `json:"serviceSelector"` @@ -88,7 +99,7 @@ type APIGateway struct { func (g APIGateway) JwksSecretMeta() metav1.ObjectMeta { return metav1.ObjectMeta{ - Name: g.Spec.JWKSSelector.Name, + Name: g.Spec.ApiEndpoint.JWKSSelector.Name, Namespace: g.Namespace, Labels: maps.Clone(g.Labels), } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 57b98dd..6aa26ea 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -93,11 +93,16 @@ func (in *APIGatewaySpec) DeepCopyInto(out *APIGatewaySpec) { *out = new(EnvoySpec) (*in).DeepCopyInto(*out) } - if in.JWKSSelector != nil { - in, out := &in.JWKSSelector, &out.JWKSSelector - *out = new(v1.SecretKeySelector) + if in.ApiEndpoint != nil { + in, out := &in.ApiEndpoint, &out.ApiEndpoint + *out = new(ApiEndpointSpec) (*in).DeepCopyInto(*out) } + if in.DashboardEndpoint != nil { + in, out := &in.DashboardEndpoint, &out.DashboardEndpoint + *out = new(DashboardEndpointSpec) + **out = **in + } if in.ServiceSelector != nil { in, out := &in.ServiceSelector, &out.ServiceSelector *out = new(metav1.LabelSelector) @@ -147,6 +152,26 @@ func (in *APIGatewayStatus) DeepCopy() *APIGatewayStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ApiEndpointSpec) DeepCopyInto(out *ApiEndpointSpec) { + *out = *in + if in.JWKSSelector != nil { + in, out := &in.JWKSSelector, &out.JWKSSelector + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApiEndpointSpec. +func (in *ApiEndpointSpec) DeepCopy() *ApiEndpointSpec { + if in == nil { + return nil + } + out := new(ApiEndpointSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthProviderMeta) DeepCopyInto(out *AuthProviderMeta) { *out = *in @@ -485,6 +510,21 @@ func (in *DashboardDbSpec) DeepCopy() *DashboardDbSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DashboardEndpointSpec) DeepCopyInto(out *DashboardEndpointSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardEndpointSpec. +func (in *DashboardEndpointSpec) DeepCopy() *DashboardEndpointSpec { + if in == nil { + return nil + } + out := new(DashboardEndpointSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DashboardList) DeepCopyInto(out *DashboardList) { *out = *in diff --git a/config/crd/bases/supabase.k8s.icb4dc0.de_apigateways.yaml b/config/crd/bases/supabase.k8s.icb4dc0.de_apigateways.yaml index aff2a06..ba7c81b 100644 --- a/config/crd/bases/supabase.k8s.icb4dc0.de_apigateways.yaml +++ b/config/crd/bases/supabase.k8s.icb4dc0.de_apigateways.yaml @@ -43,11 +43,49 @@ spec: spec: description: APIGatewaySpec defines the desired state of APIGateway. properties: + apiEndpoint: + description: |- + ApiEndpoint - Configure the endpoint for all API routes + this includes the JWT configuration + properties: + jwks: + description: JWKSSelector - selector where the JWKS can be retrieved + from to enable the API gateway to validate JWTs + 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 + required: + - jwks + type: object componentTypeLabel: default: app.kubernetes.io/name description: ComponentTypeLabel - Label to identify which Supabase component a Service represents (e.g. auth, postgrest, ...) type: string + dashboardEndpoint: + description: |- + DashboardEndpoint - Configure the endpoint for the Supabase dashboard (studio) + this includes optional authentication (basic or Oauth2) for the dashboard + type: object envoy: description: Envoy - configure the envoy instance and most importantly the control-plane @@ -2593,30 +2631,6 @@ spec: required: - controlPlane type: object - jwks: - description: JWKSSelector - selector where the JWKS can be retrieved - from to enable the API gateway to validate JWTs - 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 serviceSelector: default: matchExpressions: @@ -2674,7 +2688,6 @@ spec: x-kubernetes-map-type: atomic required: - envoy - - jwks - serviceSelector type: object status: diff --git a/config/samples/supabase_v1alpha1_apigateway.yaml b/config/samples/supabase_v1alpha1_apigateway.yaml index 711ca54..f5551a3 100644 --- a/config/samples/supabase_v1alpha1_apigateway.yaml +++ b/config/samples/supabase_v1alpha1_apigateway.yaml @@ -6,8 +6,9 @@ metadata: app.kubernetes.io/managed-by: kustomize name: gateway-sample spec: - jwks: - # will be created by Core resource operator if not present - # just make sure the secret name is either based on the name of the core resource or explicitly set - name: core-sample-jwt - key: jwks.json + apiEndpoint: + jwks: + # will be created by Core resource operator if not present + # just make sure the secret name is either based on the name of the core resource or explicitly set + name: core-sample-jwt + key: jwks.json diff --git a/docs/api/supabase.k8s.icb4dc0.de.md b/docs/api/supabase.k8s.icb4dc0.de.md index 1f783f9..b81cd28 100644 --- a/docs/api/supabase.k8s.icb4dc0.de.md +++ b/docs/api/supabase.k8s.icb4dc0.de.md @@ -71,13 +71,30 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | | `envoy` _[EnvoySpec](#envoyspec)_ | Envoy - configure the envoy instance and most importantly the control-plane | | | -| `jwks` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs | | | +| `apiEndpoint` _[ApiEndpointSpec](#apiendpointspec)_ | ApiEndpoint - Configure the endpoint for all API routes
this includes the JWT configuration | | | +| `dashboardEndpoint` _[DashboardEndpointSpec](#dashboardendpointspec)_ | DashboardEndpoint - Configure the endpoint for the Supabase dashboard (studio)
this includes optional authentication (basic or Oauth2) for the dashboard | | | | `serviceSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#labelselector-v1-meta)_ | ServiceSelector - selector to match all Supabase services (or in fact EndpointSlices) that should be considered for this APIGateway | \{ matchExpressions:[map[key:app.kubernetes.io/part-of operator:In values:[supabase]] map[key:supabase.k8s.icb4dc0.de/api-gateway-target operator:Exists]] \} | | | `componentTypeLabel` _string_ | ComponentTypeLabel - Label to identify which Supabase component a Service represents (e.g. auth, postgrest, ...) | app.kubernetes.io/name | | +#### ApiEndpointSpec + + + + + + + +_Appears in:_ +- [APIGatewaySpec](#apigatewayspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `jwks` _[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#secretkeyselector-v1-core)_ | JWKSSelector - selector where the JWKS can be retrieved from to enable the API gateway to validate JWTs | | | + + #### AuthProviderMeta @@ -319,6 +336,19 @@ _Appears in:_ | `dbCredentialsRef` _[DbCredentialsReference](#dbcredentialsreference)_ | DBCredentialsRef - reference to a Secret key where the DB credentials can be retrieved from
Credentials need to be stored in basic auth form | | | +#### DashboardEndpointSpec + + + + + + + +_Appears in:_ +- [APIGatewaySpec](#apigatewayspec) + + + #### DashboardList diff --git a/internal/controller/apigateway_controller.go b/internal/controller/apigateway_controller.go index 0a112e7..d460d64 100644 --- a/internal/controller/apigateway_controller.go +++ b/internal/controller/apigateway_controller.go @@ -53,7 +53,7 @@ var ( ) const ( - jwksSecretNameField = ".spec.jwks.name" + jwksSecretNameField = ".spec.apiEndpoint.jwks.name" ) func init() { @@ -116,7 +116,7 @@ func (r *APIGatewayReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma return nil } - return []string{gw.Spec.JWKSSelector.Name} + return []string{gw.Spec.ApiEndpoint.JWKSSelector.Name} }) if err != nil { return fmt.Errorf("setting up field index for JWKS secret name: %w", err) @@ -211,7 +211,7 @@ func (r *APIGatewayReconciler) reconcileJwksSecret( return "", err } - jwksRaw, ok := jwksSecret.Data[gateway.Spec.JWKSSelector.Key] + jwksRaw, ok := jwksSecret.Data[gateway.Spec.ApiEndpoint.JWKSSelector.Key] if !ok { return "", fmt.Errorf("%w in secret %s", ErrNoJwksConfigured, jwksSecret.Name) } @@ -401,10 +401,10 @@ func (r *APIGatewayReconciler) reconileEnvoyDeployment( { Secret: &corev1.SecretProjection{ LocalObjectReference: corev1.LocalObjectReference{ - Name: gateway.Spec.JWKSSelector.Name, + Name: gateway.Spec.ApiEndpoint.JWKSSelector.Name, }, Items: []corev1.KeyToPath{{ - Key: gateway.Spec.JWKSSelector.Key, + Key: gateway.Spec.ApiEndpoint.JWKSSelector.Key, Path: "jwks.json", }}, }, diff --git a/internal/webhook/v1alpha1/apigateway_webhook_defaulter.go b/internal/webhook/v1alpha1/apigateway_webhook_defaulter.go index fd12df3..2d86b97 100644 --- a/internal/webhook/v1alpha1/apigateway_webhook_defaulter.go +++ b/internal/webhook/v1alpha1/apigateway_webhook_defaulter.go @@ -56,8 +56,12 @@ func (d *APIGatewayCustomDefaulter) Default(ctx context.Context, obj runtime.Obj } apigatewaylog.Info("Defaulting for APIGateway", "name", apiGateway.GetName()) - if apiGateway.Spec.JWKSSelector == nil { - apiGateway.Spec.JWKSSelector = &corev1.SecretKeySelector{ + if apiGateway.Spec.ApiEndpoint == nil { + apiGateway.Spec.ApiEndpoint = new(supabasev1alpha1.ApiEndpointSpec) + } + + if apiGateway.Spec.ApiEndpoint.JWKSSelector == nil { + apiGateway.Spec.ApiEndpoint.JWKSSelector = &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ Name: supabase.ServiceConfig.JWT.ObjectName(apiGateway), },