supabase-operator/internal/webhook/v1alpha1/apigateway_webhook_validator.go

148 lines
5.5 KiB
Go

/*
Copyright 2025 Peter Kurfer.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
"context"
"errors"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
supabasev1alpha1 "code.icb4dc0.de/prskr/supabase-operator/api/v1alpha1"
)
// nolint:unused
// log is for logging in this package.
var apigatewaylog = logf.Log.WithName("apigateway-resource")
var (
ErrMissingEnvoySpec = errors.New("envoy needs to be configured")
ErrMissingControlPlaneSpec = errors.New("envoy control plane needs to be configured")
ErrOAuth2EndpointsMissing = errors.New("oauth2 endpoints missing")
ErrBasicAuthNoUsers = errors.New("no users configured for basic auth")
)
// +kubebuilder:webhook:path=/validate-supabase-k8s-icb4dc0-de-v1alpha1-apigateway,mutating=false,failurePolicy=fail,sideEffects=None,groups=supabase.k8s.icb4dc0.de,resources=apigateways,verbs=create;update,versions=v1alpha1,name=vapigateway-v1alpha1.kb.io,admissionReviewVersions=v1
// APIGatewayCustomValidator struct is responsible for validating the APIGateway resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type APIGatewayCustomValidator struct{}
var _ webhook.CustomValidator = &APIGatewayCustomValidator{}
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type APIGateway.
func (v *APIGatewayCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
apigateway, ok := obj.(*supabasev1alpha1.APIGateway)
if !ok {
return nil, fmt.Errorf("expected a APIGateway object but got %T", obj)
}
apigatewaylog.Info("Validation for APIGateway upon creation", "name", apigateway.GetName())
warnings, err := validateEnvoyControlPlane(apigateway)
if err != nil {
return warnings, err
}
if warns, err := validateDashboardEndpointSpec(apigateway); err != nil {
return append(warnings, warns...), err
} else {
warnings = append(warnings, warns...)
}
return warnings, nil
}
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type APIGateway.
func (v *APIGatewayCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
apigateway, ok := newObj.(*supabasev1alpha1.APIGateway)
if !ok {
return nil, fmt.Errorf("expected a APIGateway object for the newObj but got %T", newObj)
}
apigatewaylog.Info("Validation for APIGateway upon update", "name", apigateway.GetName())
warnings, err := validateEnvoyControlPlane(apigateway)
if err != nil {
return warnings, err
}
if warns, err := validateDashboardEndpointSpec(apigateway); err != nil {
return append(warnings, warns...), err
} else {
warnings = append(warnings, warns...)
}
return warnings, nil
}
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type APIGateway.
func (v *APIGatewayCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
apigateway, ok := obj.(*supabasev1alpha1.APIGateway)
if !ok {
return nil, fmt.Errorf("expected a APIGateway object but got %T", obj)
}
apigatewaylog.Info("Validation for APIGateway upon deletion", "name", apigateway.GetName())
return nil, nil
}
//nolint:unparam // keep the warnings for future use cases
func validateEnvoyControlPlane(gateway *supabasev1alpha1.APIGateway) (admission.Warnings, error) {
envoySpec := gateway.Spec.Envoy
if envoySpec == nil {
return nil, ErrMissingEnvoySpec
}
if envoySpec.ControlPlane == nil {
return nil, ErrMissingControlPlaneSpec
}
return nil, nil
}
func validateDashboardEndpointSpec(gateway *supabasev1alpha1.APIGateway) (warnings admission.Warnings, err error) {
dashboardEndpointSpec := gateway.Spec.DashboardEndpoint
if dashboardEndpointSpec == nil {
return nil, nil
}
switch dashboardEndpointSpec.AuthType() {
case supabasev1alpha1.DashboardAuthTypeOAuth2:
oauth2Spec := dashboardEndpointSpec.OAuth2()
if oauth2Spec.OpenIDIssuer == "" && oauth2Spec.AuthorizationEndpoint == "" && oauth2Spec.TokenEndpoint == "" {
return nil, fmt.Errorf("%w: you have to either set the OpenID issuer or authorization and token endpoints for oauth2 authentication", ErrOAuth2EndpointsMissing)
}
case supabasev1alpha1.DashboardAuthTypeBasic:
basicAuthSpec := dashboardEndpointSpec.Basic()
if len(basicAuthSpec.UsersInline) == 0 && basicAuthSpec.PlaintextUsersSecretRef == "" {
return nil, fmt.Errorf("%w: neither inline users are specified nor a secret for plaintext credentials was referenced", ErrBasicAuthNoUsers)
}
if len(basicAuthSpec.UsersInline) == 0 {
warnings = append(warnings, "no inline users were specified, make sure to have at least one username - password pair in the referenced secret otherwise the setup will be skipped")
}
}
return warnings, nil
}