diff --git a/Makefile b/Makefile index a38c5b30..0ec32a95 100644 --- a/Makefile +++ b/Makefile @@ -138,7 +138,7 @@ build-installer: set-image-controller generate ## Generate a consolidated YAML w .PHONY: set-image-controller set-image-controller: manifests kustomize - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} + cd config/manager && $(KUSTOMIZE) edit set image ghcr.io/datum-cloud/network-services-operator=${IMG} .PHONY: prepare-infra-cluster prepare-infra-cluster: cert-manager envoy-gateway external-dns diff --git a/PROJECT b/PROJECT index 73f45bea..12780bc3 100644 --- a/PROJECT +++ b/PROJECT @@ -77,8 +77,8 @@ resources: path: sigs.k8s.io/gateway-api/apis/v1 version: v1 webhooks: - validation: true defaulting: true + validation: true webhookVersion: v1 - domain: k8s.io external: true @@ -107,4 +107,40 @@ resources: kind: Domain path: go.datum.net/network-services-operator/api/v1alpha version: v1alpha +- domain: envoyproxy.io + external: true + group: gateway + kind: BackendTrafficPolicy + path: github.com/envoyproxy/gateway/api/v1alpha1 + version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 +- domain: envoyproxy.io + external: true + group: gateway + kind: SecurityPolicy + path: github.com/envoyproxy/gateway/api/v1alpha1 + version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 +- domain: envoyproxy.io + external: true + group: gateway + kind: HTTPRouteFilter + path: github.com/envoyproxy/gateway/api/v1alpha1 + version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 +- domain: envoyproxy.io + external: true + group: gateway + kind: Backend + path: github.com/envoyproxy/gateway/api/v1alpha1 + version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 version: "3" diff --git a/cmd/main.go b/cmd/main.go index 88db2416..342f4ac9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -11,6 +11,7 @@ import ( // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" multiclusterproviders "go.miloapis.com/milo/pkg/multicluster-runtime" milomulticluster "go.miloapis.com/milo/pkg/multicluster-runtime/milo" "golang.org/x/sync/errgroup" @@ -38,6 +39,7 @@ import ( networkingwebhook "go.datum.net/network-services-operator/internal/webhook" networkinggatewayv1webhooks "go.datum.net/network-services-operator/internal/webhook/v1" networkingv1alphawebhooks "go.datum.net/network-services-operator/internal/webhook/v1alpha" + webhookgatewayv1alpha1 "go.datum.net/network-services-operator/internal/webhook/v1alpha1" // +kubebuilder:scaffold:imports ) @@ -56,9 +58,11 @@ func init() { utilruntime.Must(gatewayv1.Install(scheme)) utilruntime.Must(gatewayv1alpha2.Install(scheme)) utilruntime.Must(gatewayv1alpha3.Install(scheme)) + utilruntime.Must(envoygatewayv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } +// nolint:gocyclo func main() { var enableLeaderElection bool var leaderElectionNamespace string @@ -226,6 +230,14 @@ func main() { os.Exit(1) } + if err := (&controller.GatewayResourceReplicatorReconciler{ + Config: serverConfig, + DownstreamCluster: downstreamCluster, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "GatewayResourceReplicator") + os.Exit(1) + } + if err := (&controller.DomainReconciler{ Config: serverConfig, }).SetupWithManager(mgr); err != nil { @@ -253,6 +265,26 @@ func main() { os.Exit(1) } + if err = webhookgatewayv1alpha1.SetupBackendTrafficPolicyWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "BackendTrafficPolicy") + os.Exit(1) + } + + if err = webhookgatewayv1alpha1.SetupSecurityPolicyWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "SecurityPolicy") + os.Exit(1) + } + + if err = webhookgatewayv1alpha1.SetupHTTPRouteFilterWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "HTTPRouteFilter") + os.Exit(1) + } + + if err = webhookgatewayv1alpha1.SetupBackendWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Backend") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/dev/webhook_patch.yaml b/config/dev/webhook_patch.yaml index 4abd41a8..b3068b76 100644 --- a/config/dev/webhook_patch.yaml +++ b/config/dev/webhook_patch.yaml @@ -52,6 +52,30 @@ patch: |- path: /webhooks/2/clientConfig/url - op: remove path: /webhooks/2/clientConfig/service + + - op: move + from: /webhooks/3/clientConfig/service/path + path: /webhooks/3/clientConfig/url + - op: remove + path: /webhooks/3/clientConfig/service + + - op: move + from: /webhooks/4/clientConfig/service/path + path: /webhooks/4/clientConfig/url + - op: remove + path: /webhooks/4/clientConfig/service + + - op: move + from: /webhooks/5/clientConfig/service/path + path: /webhooks/5/clientConfig/url + - op: remove + path: /webhooks/5/clientConfig/service + + - op: move + from: /webhooks/6/clientConfig/service/path + path: /webhooks/6/clientConfig/url + - op: remove + path: /webhooks/6/clientConfig/service target: kind: ValidatingWebhookConfiguration --- diff --git a/config/tools/envoy-gateway/kustomization.yaml b/config/tools/envoy-gateway/kustomization.yaml index 4d6b2ac7..9fcefddb 100644 --- a/config/tools/envoy-gateway/kustomization.yaml +++ b/config/tools/envoy-gateway/kustomization.yaml @@ -7,3 +7,12 @@ helmCharts: releaseName: envoy-gateway version: v1.5.0 repo: oci://docker.io/envoyproxy + valuesInline: + config: + envoyGateway: + extensionApis: + enableBackend: true + enableEnvoyPatchPolicy: true + runtimeFlags: + enabled: + - XDSNameSchemeV2 diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 6007c1f6..fdf2efb5 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -90,3 +90,83 @@ webhooks: resources: - httpproxies sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-gateway-envoyproxy-io-v1alpha1-backend + failurePolicy: Fail + name: vbackend-v1alpha1.kb.io + rules: + - apiGroups: + - gateway.envoyproxy.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - backends + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-gateway-envoyproxy-io-v1alpha1-backendtrafficpolicy + failurePolicy: Fail + name: vbackendtrafficpolicy-v1alpha1.kb.io + rules: + - apiGroups: + - gateway.envoyproxy.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - backendtrafficpolicies + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-gateway-envoyproxy-io-v1alpha1-httproutefilter + failurePolicy: Fail + name: vhttproutefilter-v1alpha1.kb.io + rules: + - apiGroups: + - gateway.envoyproxy.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - httproutefilters + sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-gateway-envoyproxy-io-v1alpha1-securitypolicy + failurePolicy: Fail + name: vsecuritypolicy-v1alpha1.kb.io + rules: + - apiGroups: + - gateway.envoyproxy.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - securitypolicies + sideEffects: None diff --git a/go.mod b/go.mod index 1556ea73..59eac587 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,24 @@ module go.datum.net/network-services-operator -go 1.24.0 - -toolchain go1.24.2 +go 1.24.7 require ( + github.com/envoyproxy/gateway v1.5.1 + github.com/go-logr/logr v1.4.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.37.0 github.com/stretchr/testify v1.10.0 go.miloapis.com/milo v0.1.0 - golang.org/x/sync v0.15.0 + golang.org/x/sync v0.16.0 google.golang.org/protobuf v1.36.6 - k8s.io/api v0.33.1 - k8s.io/apimachinery v0.33.2 - k8s.io/client-go v0.33.1 + k8s.io/api v0.33.3 + k8s.io/apimachinery v0.33.3 + k8s.io/client-go v0.33.3 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/gateway-api v1.3.0 + sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 ) @@ -31,12 +31,11 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/evanphx/json-patch v5.9.0+incompatible // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect @@ -45,63 +44,63 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/cel-go v0.25.0 // indirect + github.com/google/cel-go v0.26.0 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_golang v1.23.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.64.0 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.17.0 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.7 // indirect - github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/sdk v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect - go.opentelemetry.io/proto/otlp v1.6.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/proto/otlp v1.7.1 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect - golang.org/x/net v0.41.0 // indirect + golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/tools v0.35.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect - google.golang.org/grpc v1.72.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 // indirect + google.golang.org/grpc v1.74.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.33.1 // indirect - k8s.io/apiserver v0.33.1 // indirect - k8s.io/component-base v0.33.1 // indirect + k8s.io/apiextensions-apiserver v0.33.3 // indirect + k8s.io/apiserver v0.33.3 // indirect + k8s.io/component-base v0.33.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect - sigs.k8s.io/yaml v1.5.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index debede4c..26ae11d4 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,10 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= -github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/envoyproxy/gateway v1.5.1 h1:sOIcYJqu0d02H7RX/rmM3Z4yk0w8HUlo6z3A4Q8oP28= +github.com/envoyproxy/gateway v1.5.1/go.mod h1:a1Q2c/Ab61TQ4xUSS2E5wWL7Xx+HRTxEA0yqPaqUWWI= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -48,8 +50,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= -github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -62,8 +64,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -85,8 +87,9 @@ github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUt github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= @@ -100,24 +103,24 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -137,24 +140,24 @@ go.miloapis.com/milo v0.1.0 h1:AYFVz1lfta/NbWSFSSKPtnkCA2rN+iegxlfQrDgEvYY= go.miloapis.com/milo v0.1.0/go.mod h1:X+DpWOchv/Vm63mwHnboW00KRGsODY2bUTS/bBbK1+E= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= -go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= -go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= +go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -165,59 +168,59 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= +golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9 h1:WvBuA5rjZx9SNIzgcU53OohgZy6lKSus++uY4xLaWKc= -google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0 h1:0UOBWO4dC+e51ui0NFKSPbkHHiQ4TmrEfEZMLDyRmY8= +google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 h1:MAKi5q709QWfnkkpNQ0M12hYJ1+e8qYVDyowc4U1XZM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4= +google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -230,18 +233,18 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= -k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= +k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.3 h1:Wv0hGc+QFdMJB4ZSiHrCgN3zL3QRatu56+rpccKC3J4= +k8s.io/apiserver v0.33.3/go.mod h1:05632ifFEe6TxwjdAIrwINHWE2hLwyADFk5mBsQa15E= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= +k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a h1:ZV3Zr+/7s7aVbjNGICQt+ppKWsF1tehxggNfbM7XnG8= @@ -252,8 +255,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUo sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= -sigs.k8s.io/gateway-api v1.3.0 h1:q6okN+/UKDATola4JY7zXzx40WO4VISk7i9DIfOvr9M= -sigs.k8s.io/gateway-api v1.3.0/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk= +sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c h1:GS4VnGRV90GEUjrgQ2GT5ii6yzWj3KtgUg+sVMdhs5c= +sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c/go.mod h1:d8NV8nJbaRbEKem+5IuxkL8gJGOZ+FJ+NvOIltV8gDk= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 h1:Pq69tTKfN8ADw8m8A3wUtP8wJ9SPQbbOsgapm3BZEPw= @@ -264,5 +267,5 @@ sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxO sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= -sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/config/config.go b/internal/config/config.go index c7a3d71a..67a66158 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -39,6 +39,8 @@ type NetworkServicesOperator struct { Gateway GatewayConfig `json:"gateway"` + GatewayResourceReplicator GatewayResourceReplicatorConfig `json:"gatewayResourceReplicator"` + HTTPProxy HTTPProxyConfig `json:"httpProxy"` Discovery DiscoveryConfig `json:"discovery"` @@ -363,6 +365,31 @@ func (c *GatewayConfig) GatewayDNSAddress(gateway *gatewayv1.Gateway) string { // +k8s:deepcopy-gen=true +type GatewayResourceReplicatorConfig struct { + // Resources lists the upstream resource types that should be mirrored into + // the downstream control plane. + Resources []ReplicatedResourceConfig `json:"resources"` +} + +// +k8s:deepcopy-gen=true + +type ReplicatedResourceConfig struct { + // Group is the API group of the upstream resource to replicate. + Group string `json:"group"` + + // Version is the API version of the upstream resource to replicate. + Version string `json:"version"` + + // Kind is the API kind of the upstream resource to replicate. + Kind string `json:"kind"` + + // LabelSelector limits which upstream objects are replicated in the + // downstream control plane. + LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"` +} + +// +k8s:deepcopy-gen=true + type HTTPProxyConfig struct { // GatewayClassName specifies which GatewayClass to use when programming the // underlying Gateway for an HTTPProxy. @@ -393,6 +420,35 @@ func SetDefaults_GatewayConfig(obj *GatewayConfig) { } } +func SetDefaults_GatewayResourceReplicatorConfig(obj *GatewayResourceReplicatorConfig) { + if len(obj.Resources) > 0 { + return + } + + obj.Resources = []ReplicatedResourceConfig{ + {Group: "", Version: "v1", Kind: "ConfigMap", LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "networking.datumapis.com/gateway-configmap", + Operator: metav1.LabelSelectorOpExists, + }, + }, + }}, + {Group: "", Version: "v1", Kind: "Secret", LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "networking.datumapis.com/gateway-secret", + Operator: metav1.LabelSelectorOpExists, + }, + }, + }}, + {Group: "gateway.envoyproxy.io", Version: "v1alpha1", Kind: "Backend"}, + {Group: "gateway.envoyproxy.io", Version: "v1alpha1", Kind: "BackendTrafficPolicy"}, + {Group: "gateway.envoyproxy.io", Version: "v1alpha1", Kind: "SecurityPolicy"}, + {Group: "gateway.envoyproxy.io", Version: "v1alpha1", Kind: "HTTPRouteFilter"}, + } +} + func (c *GatewayConfig) IPv4Enabled() bool { return slices.Contains(c.IPFamilies, networkingv1alpha.IPv4Protocol) } diff --git a/internal/config/zz_generated.deepcopy.go b/internal/config/zz_generated.deepcopy.go index c8f2050e..d8da19bf 100644 --- a/internal/config/zz_generated.deepcopy.go +++ b/internal/config/zz_generated.deepcopy.go @@ -157,6 +157,28 @@ func (in *GatewayConfig) DeepCopy() *GatewayConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayResourceReplicatorConfig) DeepCopyInto(out *GatewayResourceReplicatorConfig) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ReplicatedResourceConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayResourceReplicatorConfig. +func (in *GatewayResourceReplicatorConfig) DeepCopy() *GatewayResourceReplicatorConfig { + if in == nil { + return nil + } + out := new(GatewayResourceReplicatorConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HTTPProxyConfig) DeepCopyInto(out *HTTPProxyConfig) { *out = *in @@ -200,6 +222,7 @@ func (in *NetworkServicesOperator) DeepCopyInto(out *NetworkServicesOperator) { in.MetricsServer.DeepCopyInto(&out.MetricsServer) in.WebhookServer.DeepCopyInto(&out.WebhookServer) in.Gateway.DeepCopyInto(&out.Gateway) + in.GatewayResourceReplicator.DeepCopyInto(&out.GatewayResourceReplicator) out.HTTPProxy = in.HTTPProxy out.Discovery = in.Discovery out.DownstreamResourceManagement = in.DownstreamResourceManagement @@ -224,6 +247,26 @@ func (in *NetworkServicesOperator) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ReplicatedResourceConfig) DeepCopyInto(out *ReplicatedResourceConfig) { + *out = *in + if in.LabelSelector != nil { + in, out := &in.LabelSelector, &out.LabelSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReplicatedResourceConfig. +func (in *ReplicatedResourceConfig) DeepCopy() *ReplicatedResourceConfig { + if in == nil { + return nil + } + out := new(ReplicatedResourceConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RetryInterval) DeepCopyInto(out *RetryInterval) { *out = *in diff --git a/internal/config/zz_generated.defaults.go b/internal/config/zz_generated.defaults.go index 9ac8830d..cab9c3f5 100644 --- a/internal/config/zz_generated.defaults.go +++ b/internal/config/zz_generated.defaults.go @@ -42,6 +42,7 @@ func SetObjectDefaults_NetworkServicesOperator(in *NetworkServicesOperator) { panic(err) } } + SetDefaults_GatewayResourceReplicatorConfig(&in.GatewayResourceReplicator) if in.HTTPProxy.GatewayClassName == "" { in.HTTPProxy.GatewayClassName = "datum-external-global-proxy" } diff --git a/internal/controller/gateway_resource_replicator_controller.go b/internal/controller/gateway_resource_replicator_controller.go new file mode 100644 index 00000000..3b7e6048 --- /dev/null +++ b/internal/controller/gateway_resource_replicator_controller.go @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package controller + +import ( + "context" + "fmt" + "maps" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mchandler "sigs.k8s.io/multicluster-runtime/pkg/handler" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + "go.datum.net/network-services-operator/internal/config" + downstreamclient "go.datum.net/network-services-operator/internal/downstreamclient" +) + +const gatewayResourceReplicatorFinalizer = "gateway.networking.datumapis.com/gateway-resource-replicator" + +type replicationResource struct { + gvk schema.GroupVersionKind +} + +// GatewayResourceReplicatorReconciler mirrors configured upstream resources into the downstream control plane. +type GatewayResourceReplicatorReconciler struct { + mgr mcmanager.Manager + Config config.NetworkServicesOperator + + DownstreamCluster cluster.Cluster + + resources map[string]replicationResource +} + +// Reconcile ensures the downstream resource mirrors the upstream resource, handling lifecycle via finalizers. +func (r *GatewayResourceReplicatorReconciler) Reconcile(ctx context.Context, req GVKRequest) (ctrl.Result, error) { + logger := log.FromContext(ctx).WithValues( + "gvk", req.GVK.String(), + "cluster", req.ClusterName, + "namespace", req.Namespace, + "name", req.Name, + ) + ctx = log.IntoContext(ctx, logger) + + if _, ok := r.resources[gvkKey(req.GVK)]; !ok { + logger.Info("gvk not configured, skipping") + return ctrl.Result{}, nil + } + + upstreamCluster, err := r.mgr.GetCluster(ctx, req.ClusterName) + if err != nil { + return ctrl.Result{}, err + } + + upstreamClient := upstreamCluster.GetClient() + + upstreamObj := &unstructured.Unstructured{} + upstreamObj.SetGroupVersionKind(req.GVK) + if err := upstreamClient.Get(ctx, req.NamespacedName, upstreamObj); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + downstreamStrategy := downstreamclient.NewMappedNamespaceResourceStrategy(req.ClusterName, upstreamClient, r.DownstreamCluster.GetClient()) + + if !upstreamObj.GetDeletionTimestamp().IsZero() { + return r.finalizeResource(ctx, upstreamClient, upstreamObj, downstreamStrategy) + } + + if !controllerutil.ContainsFinalizer(upstreamObj, gatewayResourceReplicatorFinalizer) { + controllerutil.AddFinalizer(upstreamObj, gatewayResourceReplicatorFinalizer) + if err := upstreamClient.Update(ctx, upstreamObj); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to add finalizer to %s/%s: %w", upstreamObj.GetNamespace(), upstreamObj.GetName(), err) + } + return ctrl.Result{}, nil + } + + if err := r.ensureDownstreamResource(ctx, upstreamObj, downstreamStrategy); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +func (r *GatewayResourceReplicatorReconciler) finalizeResource( + ctx context.Context, + upstreamClient client.Client, + upstreamObj *unstructured.Unstructured, + downstreamStrategy downstreamclient.ResourceStrategy, +) (ctrl.Result, error) { + if controllerutil.ContainsFinalizer(upstreamObj, gatewayResourceReplicatorFinalizer) { + if err := r.finalize(ctx, upstreamObj, downstreamStrategy); err != nil { + return ctrl.Result{}, err + } + + controllerutil.RemoveFinalizer(upstreamObj, gatewayResourceReplicatorFinalizer) + if err := upstreamClient.Update(ctx, upstreamObj); err != nil { + if apierrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to remove finalizer from %s/%s: %w", upstreamObj.GetNamespace(), upstreamObj.GetName(), err) + } + } + + return ctrl.Result{}, nil +} + +func (r *GatewayResourceReplicatorReconciler) ensureDownstreamResource( + ctx context.Context, + upstreamObj *unstructured.Unstructured, + downstreamStrategy downstreamclient.ResourceStrategy, +) error { + downstreamObjectMeta, err := downstreamStrategy.ObjectMetaFromUpstreamObject(ctx, upstreamObj) + if err != nil { + return fmt.Errorf("failed to derive downstream metadata: %w", err) + } + + downstreamObj := &unstructured.Unstructured{} + downstreamObj.SetGroupVersionKind(upstreamObj.GroupVersionKind()) + downstreamObj.SetName(downstreamObjectMeta.Name) + downstreamObj.SetNamespace(downstreamObjectMeta.Namespace) + + _, err = controllerutil.CreateOrUpdate(ctx, downstreamStrategy.GetClient(), downstreamObj, func() error { + downstreamObj.SetLabels(maps.Clone(upstreamObj.GetLabels())) + downstreamObj.SetAnnotations(maps.Clone(upstreamObj.GetAnnotations())) + + if spec, ok := upstreamObj.Object["spec"]; ok { + downstreamObj.Object["spec"] = runtime.DeepCopyJSONValue(spec) + } else { + delete(downstreamObj.Object, "spec") + } + + if data, ok := upstreamObj.Object["data"]; ok { + downstreamObj.Object["data"] = runtime.DeepCopyJSONValue(data) + } + + if err := downstreamStrategy.SetControllerReference(ctx, upstreamObj, downstreamObj); err != nil { + return fmt.Errorf("failed to set downstream controller reference: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("failed to ensure downstream resource %s/%s: %w", downstreamObj.GetNamespace(), downstreamObj.GetName(), err) + } + + // TODO(jreese) sync downstream resource status to upstream resource. + + return nil +} + +func (r *GatewayResourceReplicatorReconciler) finalize( + ctx context.Context, + upstreamObj *unstructured.Unstructured, + downstreamStrategy downstreamclient.ResourceStrategy, +) error { + downstreamObjectMeta, err := downstreamStrategy.ObjectMetaFromUpstreamObject(ctx, upstreamObj) + if err != nil { + return fmt.Errorf("failed to resolve downstream metadata for cleanup: %w", err) + } + + downstreamObj := &unstructured.Unstructured{} + downstreamObj.SetGroupVersionKind(upstreamObj.GroupVersionKind()) + downstreamObj.SetName(downstreamObjectMeta.Name) + downstreamObj.SetNamespace(downstreamObjectMeta.Namespace) + + if err := downstreamStrategy.GetClient().Delete(ctx, downstreamObj); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete downstream resource %s/%s: %w", downstreamObj.GetNamespace(), downstreamObj.GetName(), err) + } + + if err := downstreamStrategy.DeleteAnchorForObject(ctx, upstreamObj); err != nil { + return fmt.Errorf("failed to delete downstream anchor for %s/%s: %w", upstreamObj.GetNamespace(), upstreamObj.GetName(), err) + } + + return nil +} + +// SetupWithManager wires the controller for the configured resource types. +func (r *GatewayResourceReplicatorReconciler) SetupWithManager(mgr mcmanager.Manager) error { + r.mgr = mgr + + resources := make(map[string]replicationResource) + + builder := mcbuilder.TypedControllerManagedBy[GVKRequest](mgr) + + for _, resourceCfg := range r.Config.GatewayResourceReplicator.Resources { + if resourceCfg.Version == "" || resourceCfg.Kind == "" { + return fmt.Errorf("invalid gateway resource replicator config entry: %+v", resourceCfg) + } + + gvk := schema.GroupVersionKind{ + Group: resourceCfg.Group, + Version: resourceCfg.Version, + Kind: resourceCfg.Kind, + } + + selector := labels.Everything() + if resourceCfg.LabelSelector != nil { + parsedSelector, err := metav1.LabelSelectorAsSelector(resourceCfg.LabelSelector) + if err != nil { + return fmt.Errorf("failed to parse label selector for %s: %w", gvk.String(), err) + } + selector = parsedSelector + } + + resources[gvkKey(gvk)] = replicationResource{gvk: gvk} + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(gvk) + + builder = builder.Watches(obj, typedEnqueueRequestForGVK(gvk, selector)) + } + + r.resources = resources + + return builder.Named("gateway_resource_replicator").Complete(r) +} + +func typedEnqueueRequestForGVK( + gvk schema.GroupVersionKind, + selector labels.Selector, +) mchandler.TypedEventHandlerFunc[client.Object, GVKRequest] { + return func(clusterName string, cl cluster.Cluster) handler.TypedEventHandler[client.Object, GVKRequest] { + return handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []GVKRequest { + if selector != nil && !selector.Matches(labels.Set(obj.GetLabels())) { + return nil + } + + return []GVKRequest{ + { + GVK: gvk, + Request: mcreconcile.Request{ + ClusterName: clusterName, + Request: reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + }, + }, + }, + }, + } + }) + } +} + +func gvkKey(gvk schema.GroupVersionKind) string { + return gvk.String() +} diff --git a/internal/validation/backend_validation.go b/internal/validation/backend_validation.go new file mode 100644 index 00000000..68befc9c --- /dev/null +++ b/internal/validation/backend_validation.go @@ -0,0 +1,10 @@ +package validation + +import ( + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateBackend(backend *envoygatewayv1alpha1.Backend) field.ErrorList { + return nil +} diff --git a/internal/validation/backendtrafficpolicy_validation.go b/internal/validation/backendtrafficpolicy_validation.go new file mode 100644 index 00000000..c3357fcc --- /dev/null +++ b/internal/validation/backendtrafficpolicy_validation.go @@ -0,0 +1,235 @@ +package validation + +import ( + "fmt" + "time" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// TODO(jreese) move limits to config +func ValidateBackendTrafficPolicy(backendTrafficPolicy *envoygatewayv1alpha1.BackendTrafficPolicy) field.ErrorList { + allErrs := field.ErrorList{} + + specPath := field.NewPath("spec") + + // nolint:staticcheck + if backendTrafficPolicy.Spec.TargetRef != nil { + allErrs = append(allErrs, field.Forbidden(specPath.Child("targetRef"), "deprecated field, use spec.targetRefs or spec.targetSelectors instead")) + } + + allErrs = append(allErrs, validateBackendTrafficPolicyLoadBalancer(backendTrafficPolicy.Spec.LoadBalancer, specPath.Child("loadBalancer"))...) + + if backendTrafficPolicy.Spec.Retry != nil { + allErrs = append(allErrs, field.Forbidden(specPath.Child("retry"), "retry settings are not permitted")) + } + + allErrs = append(allErrs, validateBackendTrafficPolicyTCPKeepalive(backendTrafficPolicy.Spec.TCPKeepalive, specPath.Child("tcpKeepalive"))...) + allErrs = append(allErrs, validateBackendTrafficPolicyHealthCheck(backendTrafficPolicy.Spec.HealthCheck, specPath.Child("healthCheck"))...) + allErrs = append(allErrs, validateBackendTrafficPolicyTimeout(backendTrafficPolicy.Spec.Timeout, specPath.Child("timeout"))...) + allErrs = append(allErrs, validateBackendTrafficPolicyConnection(backendTrafficPolicy.Spec.Connection, specPath.Child("connection"))...) + allErrs = append(allErrs, validateBackendTrafficPolicyDNS(backendTrafficPolicy.Spec.DNS, specPath.Child("dns"))...) + allErrs = append(allErrs, validateBackendTrafficPolicyHTTP2(backendTrafficPolicy.Spec.HTTP2, specPath.Child("http2"))...) + allErrs = append(allErrs, validateBackendTrafficPolicyRateLimit(backendTrafficPolicy.Spec.RateLimit, specPath.Child("rateLimit"))...) + allErrs = append(allErrs, validateBackendTrafficPolicyFaultInjection(backendTrafficPolicy.Spec.FaultInjection, specPath.Child("faultInjection"))...) + + return allErrs +} + +func validateBackendTrafficPolicyLoadBalancer(loadBalancer *envoygatewayv1alpha1.LoadBalancer, fldPath *field.Path) field.ErrorList { + if loadBalancer == nil { + return nil + } + + allErrs := field.ErrorList{} + + if loadBalancer.EndpointOverride != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("endpointOverride"), "loadbalancer endpoint overrides are not permitted")) + } + + return allErrs +} + +func validateBackendTrafficPolicyTCPKeepalive(tcpKeepalive *envoygatewayv1alpha1.TCPKeepalive, fldPath *field.Path) field.ErrorList { + if tcpKeepalive == nil { + return nil + } + + allErrs := field.ErrorList{} + + if tcpKeepalive.Probes != nil && *tcpKeepalive.Probes < 9 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("probe"), *tcpKeepalive.Probes, "must be greater than or equal to 9")) + } + + if v := tcpKeepalive.IdleTime; v != nil { + idleTimeFieldPath := fldPath.Child("idleTime") + allErrs = append(allErrs, validateGatewayDuration(idleTimeFieldPath, v, ptr.To(5*time.Minute), nil)...) + } + + if v := tcpKeepalive.Interval; v != nil { + intervalFieldPath := fldPath.Child("interval") + allErrs = append(allErrs, validateGatewayDuration(intervalFieldPath, v, ptr.To(30*time.Second), nil)...) + } + + return allErrs +} + +func validateBackendTrafficPolicyHealthCheck(healthCheck *envoygatewayv1alpha1.HealthCheck, fldPath *field.Path) field.ErrorList { + if healthCheck == nil { + return nil + } + + allErrs := field.ErrorList{} + + if healthCheck.Active != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("active"), "active health checks are not permitted")) + } + + return allErrs +} + +func validateBackendTrafficPolicyTimeout(timeout *envoygatewayv1alpha1.Timeout, fldPath *field.Path) field.ErrorList { + if timeout == nil { + return nil + } + + allErrs := field.ErrorList{} + + if timeout.TCP != nil { + if v := timeout.TCP.ConnectTimeout; v != nil { + connectTimeoutFieldPath := fldPath.Child("tcp").Child("connectTimeout") + allErrs = append(allErrs, validateGatewayDuration(connectTimeoutFieldPath, v, nil, ptr.To(1*time.Hour))...) + } + } + + if timeout.HTTP != nil { + httpTimeoutFieldPath := fldPath.Child("http") + + if v := timeout.HTTP.ConnectionIdleTimeout; v != nil { + connectionIdleTimeoutFieldPath := httpTimeoutFieldPath.Child("connectionIdleTimeout") + allErrs = append(allErrs, validateGatewayDuration(connectionIdleTimeoutFieldPath, v, nil, ptr.To(1*time.Hour))...) + } + + if v := timeout.HTTP.MaxConnectionDuration; v != nil { + maxConnectionDurationFieldPath := httpTimeoutFieldPath.Child("maxConnectionDuration") + allErrs = append(allErrs, validateGatewayDuration(maxConnectionDurationFieldPath, v, nil, ptr.To(1*time.Hour))...) + } + + if v := timeout.HTTP.RequestTimeout; v != nil { + requestTimeoutFieldPath := httpTimeoutFieldPath.Child("requestTimeout") + allErrs = append(allErrs, validateGatewayDuration(requestTimeoutFieldPath, v, nil, ptr.To(1*time.Hour))...) + } + + } + + return allErrs +} + +func validateBackendTrafficPolicyConnection(connection *envoygatewayv1alpha1.BackendConnection, fldPath *field.Path) field.ErrorList { + if connection == nil { + return nil + } + + limit := resource.MustParse("512Ki") + if connection.BufferLimit != nil && connection.BufferLimit.Cmp(limit) == 1 { + return field.ErrorList{ + field.Invalid(fldPath.Child("bufferLimit"), connection.BufferLimit.String(), fmt.Sprintf("must be less than or equal to %s", limit.String())), + } + } + + return nil +} + +func validateBackendTrafficPolicyDNS(dns *envoygatewayv1alpha1.DNS, fldPath *field.Path) field.ErrorList { + if dns == nil { + return nil + } + + allErrs := field.ErrorList{} + + if v := dns.DNSRefreshRate; v != nil { + dnsRefreshRateFieldPath := fldPath.Child("dnsRefreshRate") + allErrs = append(allErrs, validateGatewayDuration(dnsRefreshRateFieldPath, v, ptr.To(30*time.Second), nil)...) + } + + if dns.RespectDNSTTL != nil && !*dns.RespectDNSTTL { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("respectDnsTtl"), "must respect DNS TTL")) + } + + return allErrs +} + +func validateBackendTrafficPolicyHTTP2(http2 *envoygatewayv1alpha1.HTTP2Settings, fldPath *field.Path) field.ErrorList { + if http2 == nil { + return nil + } + + allErrs := field.ErrorList{} + if http2.InitialStreamWindowSize != nil { + limit := resource.MustParse("64Ki") + if http2.InitialStreamWindowSize.Cmp(limit) == 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initialStreamWindowSize"), http2.InitialStreamWindowSize.String(), fmt.Sprintf("must be less than or equal to %s", limit.String()))) + } + } + + if http2.InitialConnectionWindowSize != nil { + limit := resource.MustParse("1Mi") + if http2.InitialConnectionWindowSize.Cmp(limit) == 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initialConnectionWindowSize"), http2.InitialConnectionWindowSize.String(), fmt.Sprintf("must be less than or equal to %s", limit.String()))) + } + } + + if http2.MaxConcurrentStreams != nil { + limit := uint32(1024) + if *http2.MaxConcurrentStreams > limit { + allErrs = append(allErrs, field.Invalid(fldPath.Child("maxConcurrentStreams"), http2.MaxConcurrentStreams, fmt.Sprintf("must be less than or equal to %d", limit))) + } + } + + return allErrs +} + +func validateBackendTrafficPolicyRateLimit(rateLimit *envoygatewayv1alpha1.RateLimitSpec, fldPath *field.Path) field.ErrorList { + if rateLimit == nil { + return nil + } + + allErrs := field.ErrorList{} + + if rateLimit.Type != envoygatewayv1alpha1.LocalRateLimitType { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), rateLimit.Type, []envoygatewayv1alpha1.RateLimitType{envoygatewayv1alpha1.LocalRateLimitType})) + } + + if rateLimit.Global != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("global"), "global rate limits are not permitted")) + } + + return allErrs +} + +func validateBackendTrafficPolicyFaultInjection(faultInjection *envoygatewayv1alpha1.FaultInjection, fldPath *field.Path) field.ErrorList { + if faultInjection != nil { + return field.ErrorList{ + field.Forbidden(fldPath, "fault injection is not permitted"), + } + } + + return nil +} + +func validateGatewayDuration(fldPath *field.Path, gatewayDuration *gatewayv1.Duration, lowerLimit *time.Duration, upperLimit *time.Duration) field.ErrorList { + allErrs := field.ErrorList{} + if d, err := time.ParseDuration(string(*gatewayDuration)); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath, *gatewayDuration, "invalid duration")) + } else if lowerLimit != nil && d < *lowerLimit { + allErrs = append(allErrs, field.Invalid(fldPath, *gatewayDuration, fmt.Sprintf("duration must be greater than or equal to %s", *lowerLimit))) + } else if upperLimit != nil && d > *upperLimit { + allErrs = append(allErrs, field.Invalid(fldPath, *gatewayDuration, fmt.Sprintf("duration must be less than or equal to %s", *upperLimit))) + } + + return allErrs +} diff --git a/internal/validation/backendtrafficpolicy_validation_test.go b/internal/validation/backendtrafficpolicy_validation_test.go new file mode 100644 index 00000000..22ab3c5f --- /dev/null +++ b/internal/validation/backendtrafficpolicy_validation_test.go @@ -0,0 +1,45 @@ +package validation + +import ( + "testing" + + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +func TestValidateBackendTrafficPolicy(t *testing.T) { + scenarios := map[string]struct { + backendTrafficPolicy *envoygatewayv1alpha1.BackendTrafficPolicy + expectedErrors field.ErrorList + }{ + "spec.targetRef forbidden": { + backendTrafficPolicy: &envoygatewayv1alpha1.BackendTrafficPolicy{ + Spec: envoygatewayv1alpha1.BackendTrafficPolicySpec{ + PolicyTargetReferences: envoygatewayv1alpha1.PolicyTargetReferences{ + TargetRef: &gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName{ + SectionName: ptr.To(gatewayv1.SectionName("test")), + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Forbidden(field.NewPath("spec", "targetRef"), "Invalid"), + }, + }, + } + + for name, scenario := range scenarios { + t.Run(name, func(t *testing.T) { + errs := ValidateBackendTrafficPolicy(scenario.backendTrafficPolicy) + delta := cmp.Diff(scenario.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")) + if delta != "" { + t.Errorf("Testcase %s - expected errors '%v', got '%v', diff: '%v'", name, scenario.expectedErrors, errs, delta) + } + }) + } +} diff --git a/internal/validation/httproutefilter_validation.go b/internal/validation/httproutefilter_validation.go new file mode 100644 index 00000000..55acc648 --- /dev/null +++ b/internal/validation/httproutefilter_validation.go @@ -0,0 +1,10 @@ +package validation + +import ( + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateHTTPRouteFilter(httpRouteFilter *envoygatewayv1alpha1.HTTPRouteFilter) field.ErrorList { + return nil +} diff --git a/internal/validation/securitypolicy_validation.go b/internal/validation/securitypolicy_validation.go new file mode 100644 index 00000000..fc7492c9 --- /dev/null +++ b/internal/validation/securitypolicy_validation.go @@ -0,0 +1,10 @@ +package validation + +import ( + envoygatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func ValidateSecurityPolicy(securityPolicy *envoygatewayv1alpha1.SecurityPolicy) field.ErrorList { + return nil +} diff --git a/internal/webhook/v1alpha1/backend_webhook.go b/internal/webhook/v1alpha1/backend_webhook.go new file mode 100644 index 00000000..1edd24b8 --- /dev/null +++ b/internal/webhook/v1alpha1/backend_webhook.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "fmt" + + gatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "go.datum.net/network-services-operator/internal/validation" +) + +// SetupBackendWebhookWithManager registers the webhook for Backend in the manager. +func SetupBackendWebhookWithManager(mgr mcmanager.Manager) error { + return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.Backend{}). + WithValidator(&BackendCustomValidator{mgr: mgr}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-gateway-envoyproxy-io-v1alpha1-backend,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.envoyproxy.io,resources=backends,verbs=create;update,versions=v1alpha1,name=vbackend-v1alpha1.kb.io,admissionReviewVersions=v1 + +type BackendCustomValidator struct { + mgr mcmanager.Manager +} + +var _ webhook.CustomValidator = &BackendCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Backend. +func (v *BackendCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + backend, ok := obj.(*gatewayv1alpha1.Backend) + if !ok { + return nil, fmt.Errorf("expected a Backend object but got %T", obj) + } + + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok { + return nil, fmt.Errorf("expected a cluster name in the context") + } + + log := logf.FromContext(ctx).WithValues("cluster", clusterName) + log.Info("Validating Backend", "name", backend.GetName(), "cluster", clusterName) + + if errs := validation.ValidateBackend(backend); len(errs) > 0 { + return nil, apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), backend.GetName(), errs) + } + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Backend. +func (v *BackendCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + backend, ok := newObj.(*gatewayv1alpha1.Backend) + if !ok { + return nil, fmt.Errorf("expected a Backend object but got %T", newObj) + } + + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok { + return nil, fmt.Errorf("expected a cluster name in the context") + } + + log := logf.FromContext(ctx).WithValues("cluster", clusterName) + log.Info("Validating Backend", "name", backend.GetName(), "cluster", clusterName) + + if errs := validation.ValidateBackend(backend); len(errs) > 0 { + return nil, apierrors.NewInvalid(oldObj.GetObjectKind().GroupVersionKind().GroupKind(), backend.GetName(), errs) + } + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Backend. +func (v *BackendCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/internal/webhook/v1alpha1/backendtrafficpolicy_webhook.go b/internal/webhook/v1alpha1/backendtrafficpolicy_webhook.go new file mode 100644 index 00000000..fd8199e6 --- /dev/null +++ b/internal/webhook/v1alpha1/backendtrafficpolicy_webhook.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "fmt" + + gatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "go.datum.net/network-services-operator/internal/validation" +) + +// SetupBackendTrafficPolicyWebhookWithManager registers the webhook for BackendTrafficPolicy in the manager. +func SetupBackendTrafficPolicyWebhookWithManager(mgr mcmanager.Manager) error { + return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.BackendTrafficPolicy{}). + WithValidator(&BackendTrafficPolicyCustomValidator{mgr: mgr}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-gateway-envoyproxy-io-v1alpha1-backendtrafficpolicy,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.envoyproxy.io,resources=backendtrafficpolicies,verbs=create;update,versions=v1alpha1,name=vbackendtrafficpolicy-v1alpha1.kb.io,admissionReviewVersions=v1 + +type BackendTrafficPolicyCustomValidator struct { + mgr mcmanager.Manager +} + +var _ webhook.CustomValidator = &BackendTrafficPolicyCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type BackendTrafficPolicy. +func (v *BackendTrafficPolicyCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + backendTrafficPolicy, ok := obj.(*gatewayv1alpha1.BackendTrafficPolicy) + if !ok { + return nil, fmt.Errorf("expected a BackendTrafficPolicy object but got %T", obj) + } + + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok { + return nil, fmt.Errorf("expected a cluster name in the context") + } + + log := logf.FromContext(ctx).WithValues("cluster", clusterName) + log.Info("Validating BackendTrafficPolicy", "name", backendTrafficPolicy.GetName(), "cluster", clusterName) + + if errs := validation.ValidateBackendTrafficPolicy(backendTrafficPolicy); len(errs) > 0 { + return nil, apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), backendTrafficPolicy.GetName(), errs) + } + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type BackendTrafficPolicy. +func (v *BackendTrafficPolicyCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + backendTrafficPolicy, ok := newObj.(*gatewayv1alpha1.BackendTrafficPolicy) + if !ok { + return nil, fmt.Errorf("expected a BackendTrafficPolicy object for the newObj but got %T", newObj) + } + + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok { + return nil, fmt.Errorf("expected a cluster name in the context") + } + + log := logf.FromContext(ctx).WithValues("cluster", clusterName) + log.Info("Validating BackendTrafficPolicy", "name", backendTrafficPolicy.GetName(), "cluster", clusterName) + + if errs := validation.ValidateBackendTrafficPolicy(backendTrafficPolicy); len(errs) > 0 { + return nil, apierrors.NewInvalid(oldObj.GetObjectKind().GroupVersionKind().GroupKind(), backendTrafficPolicy.GetName(), errs) + } + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type BackendTrafficPolicy. +func (v *BackendTrafficPolicyCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/internal/webhook/v1alpha1/httproutefilter_webhook.go b/internal/webhook/v1alpha1/httproutefilter_webhook.go new file mode 100644 index 00000000..18f65228 --- /dev/null +++ b/internal/webhook/v1alpha1/httproutefilter_webhook.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "fmt" + + gatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "go.datum.net/network-services-operator/internal/validation" +) + +// SetupHTTPRouteFilterWebhookWithManager registers the webhook for HTTPRouteFilter in the manager. +func SetupHTTPRouteFilterWebhookWithManager(mgr mcmanager.Manager) error { + return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.HTTPRouteFilter{}). + WithValidator(&HTTPRouteFilterCustomValidator{mgr: mgr}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-gateway-envoyproxy-io-v1alpha1-httproutefilter,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.envoyproxy.io,resources=httproutefilters,verbs=create;update,versions=v1alpha1,name=vhttproutefilter-v1alpha1.kb.io,admissionReviewVersions=v1 + +type HTTPRouteFilterCustomValidator struct { + mgr mcmanager.Manager +} + +var _ webhook.CustomValidator = &HTTPRouteFilterCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type HTTPRouteFilter. +func (v *HTTPRouteFilterCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + httpRouteFilter, ok := obj.(*gatewayv1alpha1.HTTPRouteFilter) + if !ok { + return nil, fmt.Errorf("expected a HTTPRouteFilter object but got %T", obj) + } + + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok { + return nil, fmt.Errorf("expected a cluster name in the context") + } + + log := logf.FromContext(ctx).WithValues("cluster", clusterName) + log.Info("Validating HTTPRouteFilter", "name", httpRouteFilter.GetName(), "cluster", clusterName) + + if errs := validation.ValidateHTTPRouteFilter(httpRouteFilter); len(errs) > 0 { + return nil, apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), httpRouteFilter.GetName(), errs) + } + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type HTTPRouteFilter. +func (v *HTTPRouteFilterCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + httpRouteFilter, ok := newObj.(*gatewayv1alpha1.HTTPRouteFilter) + if !ok { + return nil, fmt.Errorf("expected a HTTPRouteFilter object but got %T", newObj) + } + + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok { + return nil, fmt.Errorf("expected a cluster name in the context") + } + + log := logf.FromContext(ctx).WithValues("cluster", clusterName) + log.Info("Validating HTTPRouteFilter", "name", httpRouteFilter.GetName(), "cluster", clusterName) + + if errs := validation.ValidateHTTPRouteFilter(httpRouteFilter); len(errs) > 0 { + return nil, apierrors.NewInvalid(oldObj.GetObjectKind().GroupVersionKind().GroupKind(), httpRouteFilter.GetName(), errs) + } + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type HTTPRouteFilter. +func (v *HTTPRouteFilterCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/internal/webhook/v1alpha1/securitypolicy_webhook.go b/internal/webhook/v1alpha1/securitypolicy_webhook.go new file mode 100644 index 00000000..e96c942f --- /dev/null +++ b/internal/webhook/v1alpha1/securitypolicy_webhook.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package v1alpha1 + +import ( + "context" + "fmt" + + gatewayv1alpha1 "github.com/envoyproxy/gateway/api/v1alpha1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + + "go.datum.net/network-services-operator/internal/validation" +) + +// SetupSecurityPolicyWebhookWithManager registers the webhook for SecurityPolicy in the manager. +func SetupSecurityPolicyWebhookWithManager(mgr mcmanager.Manager) error { + return ctrl.NewWebhookManagedBy(mgr.GetLocalManager()).For(&gatewayv1alpha1.SecurityPolicy{}). + WithValidator(&SecurityPolicyCustomValidator{mgr: mgr}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-gateway-envoyproxy-io-v1alpha1-securitypolicy,mutating=false,failurePolicy=fail,sideEffects=None,groups=gateway.envoyproxy.io,resources=securitypolicies,verbs=create;update,versions=v1alpha1,name=vsecuritypolicy-v1alpha1.kb.io,admissionReviewVersions=v1 + +type SecurityPolicyCustomValidator struct { + mgr mcmanager.Manager +} + +var _ webhook.CustomValidator = &SecurityPolicyCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type SecurityPolicy. +func (v *SecurityPolicyCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + securityPolicy, ok := obj.(*gatewayv1alpha1.SecurityPolicy) + if !ok { + return nil, fmt.Errorf("expected a SecurityPolicy object but got %T", obj) + } + + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok { + return nil, fmt.Errorf("expected a cluster name in the context") + } + + log := logf.FromContext(ctx).WithValues("cluster", clusterName) + log.Info("Validating SecurityPolicy", "name", securityPolicy.GetName(), "cluster", clusterName) + + if errs := validation.ValidateSecurityPolicy(securityPolicy); len(errs) > 0 { + return nil, apierrors.NewInvalid(obj.GetObjectKind().GroupVersionKind().GroupKind(), securityPolicy.GetName(), errs) + } + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type SecurityPolicy. +func (v *SecurityPolicyCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + securityPolicy, ok := newObj.(*gatewayv1alpha1.SecurityPolicy) + if !ok { + return nil, fmt.Errorf("expected a SecurityPolicy object but got %T", newObj) + } + + clusterName, ok := mccontext.ClusterFrom(ctx) + if !ok { + return nil, fmt.Errorf("expected a cluster name in the context") + } + + log := logf.FromContext(ctx).WithValues("cluster", clusterName) + log.Info("Validating SecurityPolicy", "name", securityPolicy.GetName(), "cluster", clusterName) + + if errs := validation.ValidateSecurityPolicy(securityPolicy); len(errs) > 0 { + return nil, apierrors.NewInvalid(oldObj.GetObjectKind().GroupVersionKind().GroupKind(), securityPolicy.GetName(), errs) + } + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type SecurityPolicy. +func (v *SecurityPolicyCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + return nil, nil +}