diff --git a/cmd/api-syncagent/main.go b/cmd/api-syncagent/main.go index 218f7a9..3d6688a 100644 --- a/cmd/api-syncagent/main.go +++ b/cmd/api-syncagent/main.go @@ -93,6 +93,8 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error { hello.Info("Moin, I'm the kcp Sync Agent") + urlRewriter := NewURLRewriter(opts) + // create the ctrl-runtime manager mgr, err := setupLocalManager(ctx, opts) if err != nil { @@ -155,7 +157,7 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error { // regular mcmanager, capable of starting new controllers at any later time and // allowing them to be also stopped at any time. The syncmanager needs it to // start/stop sync controllers for each PublishedResource. - dmcm, err := kcp.NewDynamicMultiClusterManager(endpoint.EndpointSlice.Config, endpoint.EndpointSlice.Name) + dmcm, err := kcp.NewDynamicMultiClusterManager(endpoint.EndpointSlice.Config, endpoint.EndpointSlice.Name, urlRewriter) if err != nil { return fmt.Errorf("failed to start dynamic multi cluster manager: %w", err) } @@ -194,7 +196,7 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error { if err := startController("sync", func() error { // The syncmanager needs to be able to determine whether an API is already bound and available // before it can start any sync controllers. That discovery logic is encapsulated in the ResourceProber. - prober := discovery.NewResourceProber(endpoint.EndpointSlice.Config, endpointSliceCluster.GetClient(), endpoint.EndpointSlice.Name) + prober := discovery.NewResourceProber(endpoint.EndpointSlice.Config, endpointSliceCluster.GetClient(), endpoint.EndpointSlice.Name, urlRewriter) return syncmanager.Add(ctx, mgr, prober, dmcm, log, opts.PublishedResourceSelector, opts.Namespace, opts.AgentName) }); err != nil { diff --git a/cmd/api-syncagent/options.go b/cmd/api-syncagent/options.go index 0ad8aba..2a9a59d 100644 --- a/cmd/api-syncagent/options.go +++ b/cmd/api-syncagent/options.go @@ -19,9 +19,13 @@ package main import ( "errors" "fmt" + "net" + "net/url" + "strings" "github.com/spf13/pflag" + "github.com/kcp-dev/api-syncagent/internal/kcp" "github.com/kcp-dev/api-syncagent/internal/log" "k8s.io/apimachinery/pkg/labels" @@ -65,6 +69,14 @@ type Options struct { KubeconfigHostOverride string KubeconfigCAFileOverride string + // APIExportHostPortOverrides allows overriding the host:port of URLs found in + // APIExportEndpointSlice. Format: "original-host:port=new-host:port". + // Can be specified multiple times. + APIExportHostPortOverrides []string + + // parsedHostPortOverrides stores the parsed overrides (original -> new). + parsedHostPortOverrides []hostPortOverride + LogOptions log.Options MetricsAddr string @@ -73,6 +85,11 @@ type Options struct { DisabledControllers []string } +type hostPortOverride struct { + Original string + New string +} + func NewOptions() *Options { return &Options{ LogOptions: log.NewDefaultOptions(), @@ -92,6 +109,7 @@ func (o *Options) AddFlags(flags *pflag.FlagSet) { flags.BoolVar(&o.EnableLeaderElection, "enable-leader-election", o.EnableLeaderElection, "whether to perform leader election") flags.StringVar(&o.KubeconfigHostOverride, "kubeconfig-host-override", o.KubeconfigHostOverride, "override the host configured in the local kubeconfig") flags.StringVar(&o.KubeconfigCAFileOverride, "kubeconfig-ca-file-override", o.KubeconfigCAFileOverride, "override the server CA file configured in the local kubeconfig") + flags.StringSliceVar(&o.APIExportHostPortOverrides, "apiexport-hostport-override", o.APIExportHostPortOverrides, "override the host:port in APIExportEndpointSlice URLs (format: original-host:port=new-host:port, can be specified multiple times)") flags.StringVar(&o.MetricsAddr, "metrics-address", o.MetricsAddr, "host and port to serve Prometheus metrics via /metrics (HTTP)") flags.StringVar(&o.HealthAddr, "health-address", o.HealthAddr, "host and port to serve probes via /readyz and /healthz (HTTP)") flags.StringSliceVar(&o.DisabledControllers, "disabled-controllers", o.DisabledControllers, fmt.Sprintf("comma-separated list of controllers (out of %v) to disable (can be given multiple times)", sets.List(availableControllers))) @@ -135,9 +153,31 @@ func (o *Options) Validate() error { errs = append(errs, fmt.Errorf("unknown controller(s) %v, mut be any of %v", sets.List(unknown), sets.List(availableControllers))) } + for i, s := range o.APIExportHostPortOverrides { + if err := validateHostPortOverride(s); err != nil { + errs = append(errs, fmt.Errorf("invalid --apiexport-hostport-override #%d %q: %w", i+1, s, err)) + } + } + return utilerrors.NewAggregate(errs) } +func validateHostPortOverride(s string) error { + parts := strings.Split(s, "=") + if len(parts) != 2 { + return errors.New("format must be 'original-host:port=new-host:port'") + } + + // validate that both sides have valid host:port combination + for i, part := range parts { + if _, _, err := net.SplitHostPort(part); err != nil { + return fmt.Errorf("part %d is not a valid host:port: %w", i+1, err) + } + } + + return nil +} + func (o *Options) Complete() error { errs := []error{} @@ -153,5 +193,41 @@ func (o *Options) Complete() error { o.PublishedResourceSelector = selector } + for _, s := range o.APIExportHostPortOverrides { + parts := strings.Split(s, "=") + o.parsedHostPortOverrides = append(o.parsedHostPortOverrides, hostPortOverride{ + Original: parts[0], + New: parts[1], + }) + } + return utilerrors.NewAggregate(errs) } + +// NewURLRewriter returns a new URL rewriter that applies the host:port overrides +// to the given URL if configured. It applies all configured overrides in order. +func NewURLRewriter(o *Options) kcp.URLRewriterFunc { + if len(o.parsedHostPortOverrides) == 0 { + return func(url string) (string, error) { + return url, nil // NOP + } + } + + return func(rawURL string) (string, error) { + parsed, err := url.Parse(rawURL) + if err != nil { + return rawURL, err + } + + currentHost := parsed.Host + for _, override := range o.parsedHostPortOverrides { + if currentHost == override.Original { + currentHost = override.New + } + } + + parsed.Host = currentHost + + return parsed.String(), nil + } +} diff --git a/cmd/api-syncagent/options_test.go b/cmd/api-syncagent/options_test.go new file mode 100644 index 0000000..d3f5d99 --- /dev/null +++ b/cmd/api-syncagent/options_test.go @@ -0,0 +1,193 @@ +/* +Copyright 2025 The KCP Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func TestURLRewriter(t *testing.T) { + tests := []struct { + name string + overrides []string + inputURL string + expectedURL string + expectParseError bool + }{ + { + name: "no override configured", + overrides: []string{}, + inputURL: "https://kcp.example.com:6443", + expectedURL: "https://kcp.example.com:6443", + }, + { + name: "single override host and port", + overrides: []string{"kcp.example.com:6443=localhost:8443"}, + inputURL: "https://kcp.example.com:6443/clusters/root", + expectedURL: "https://localhost:8443/clusters/root", + }, + { + name: "override only affects matching host:port", + overrides: []string{"kcp.example.com:6443=localhost:8443"}, + inputURL: "https://other.example.com:6443/clusters/root", + expectedURL: "https://other.example.com:6443/clusters/root", + }, + { + name: "override with different port", + overrides: []string{"kcp.example.com:443=localhost:30443"}, + inputURL: "https://kcp.example.com:443", + expectedURL: "https://localhost:30443", + }, + { + name: "multiple overrides applied in order", + overrides: []string{ + "kcp.example.com:6443=kcp-proxy:6443", + "kcp-proxy:6443=localhost:8443", + }, + inputURL: "https://kcp.example.com:6443/clusters/root", + expectedURL: "https://localhost:8443/clusters/root", + }, + { + name: "multiple independent overrides", + overrides: []string{ + "kcp1.example.com:6443=localhost:8443", + "kcp2.example.com:6443=localhost:9443", + }, + inputURL: "https://kcp2.example.com:6443/api", + expectedURL: "https://localhost:9443/api", + }, + { + name: "override with query parameters", + overrides: []string{"kcp.example.com:6443=localhost:8443"}, + inputURL: "https://kcp.example.com:6443/api?param=value", + expectedURL: "https://localhost:8443/api?param=value", + }, + { + name: "override with fragment", + overrides: []string{"kcp.example.com:6443=localhost:8443"}, + inputURL: "https://kcp.example.com:6443/api#section", + expectedURL: "https://localhost:8443/api#section", + }, + { + name: "override with IPv6", + overrides: []string{"[::1]:6443=localhost:8443"}, + inputURL: "https://[::1]:6443/clusters/root", + expectedURL: "https://localhost:8443/clusters/root", + }, + { + name: "override to IPv6", + overrides: []string{"kcp.example.com:6443=[2001:db8::1]:8443"}, + inputURL: "https://kcp.example.com:6443/clusters/root", + expectedURL: "https://[2001:db8::1]:8443/clusters/root", + }, + { + name: "override preserves scheme", + overrides: []string{"kcp.example.com:6443=localhost:8443"}, + inputURL: "http://kcp.example.com:6443/api", + expectedURL: "http://localhost:8443/api", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := NewOptions() + opts.APIExportHostPortOverrides = tt.overrides + + // Complete to parse the overrides. + if err := opts.Complete(); err != nil { + if !tt.expectParseError { + t.Errorf("Complete() failed: %v", err) + } + return + } + + if tt.expectParseError { + t.Error("Expected Complete() to fail but it succeeded") + return + } + + rewriter := NewURLRewriter(opts) + + result, err := rewriter(tt.inputURL) + if err != nil { + t.Fatalf("URLRewriter() returned unexpected error: %v", err) + } + + if result != tt.expectedURL { + t.Errorf("Expected %q, but got %q", tt.expectedURL, result) + } + }) + } +} + +func TestValidateHostPortOverride(t *testing.T) { + tests := []struct { + name string + override string + expectError bool + }{ + { + name: "valid override", + override: "kcp.example.com:6443=localhost:8443", + expectError: false, + }, + { + name: "valid IPv6 override", + override: "[::1]:6443=[2001:db8::1]:8443", + expectError: false, + }, + { + name: "valid IPv6 to IPv4 override", + override: "[::1]:6443=localhost:8443", + expectError: false, + }, + { + name: "missing equals sign", + override: "kcp.example.com:6443", + expectError: true, + }, + { + name: "missing port on left side", + override: "kcp.example.com=localhost:8443", + expectError: true, + }, + { + name: "missing port on right side", + override: "kcp.example.com:6443=localhost", + expectError: true, + }, + { + name: "too many equals signs", + override: "kcp.example.com:6443=localhost:8443=extra", + expectError: true, + }, + { + name: "IPv6 without brackets", + override: "::1:6443=localhost:8443", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateHostPortOverride(tt.override) + if (err != nil) != tt.expectError { + t.Fatalf("returned error = %v, expectError %v", err, tt.expectError) + } + }) + } +} diff --git a/go.mod b/go.mod index 4aa09a0..ccf2e43 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.25.7 replace github.com/kcp-dev/api-syncagent/sdk => ./sdk replace ( - k8s.io/apiextensions-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20251209073509-71e0f2506325 - k8s.io/apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20251209073509-71e0f2506325 + k8s.io/apiextensions-apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20251216144411-4b3495fdcb9d + k8s.io/apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20251216144411-4b3495fdcb9d ) require ( @@ -18,10 +18,9 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/kcp-dev/api-syncagent/sdk v0.0.0-00010101000000-000000000000 - github.com/kcp-dev/kcp v0.29.1-0.20251210093424-08fb9eb48494 github.com/kcp-dev/logicalcluster/v3 v3.0.5 - github.com/kcp-dev/multicluster-provider v0.3.2-0.20251209135920-e758bf0f4e48 - github.com/kcp-dev/sdk v0.28.1-0.20251209130449-436a0347809b + github.com/kcp-dev/multicluster-provider v0.5.0 + github.com/kcp-dev/sdk v0.30.0 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 @@ -29,16 +28,17 @@ require ( github.com/tidwall/sjson v1.2.5 go.uber.org/zap v1.27.1 k8c.io/reconciler v0.5.0 - k8s.io/api v0.34.2 - k8s.io/apiextensions-apiserver v0.34.2 - k8s.io/apimachinery v0.34.2 - k8s.io/apiserver v0.34.2 - k8s.io/client-go v0.34.2 - k8s.io/component-base v0.34.2 - k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 - sigs.k8s.io/controller-runtime v0.22.4 - sigs.k8s.io/multicluster-runtime v0.22.4-beta.1 + k8s.io/api v0.35.0 + k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery v0.35.0 + k8s.io/apiserver v0.35.0 + k8s.io/client-go v0.35.0 + k8s.io/component-base v0.35.0 + k8s.io/klog/v2 v2.130.1 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + sigs.k8s.io/controller-runtime v0.23.3 + sigs.k8s.io/multicluster-runtime v0.23.1 sigs.k8s.io/yaml v1.6.0 ) @@ -47,21 +47,14 @@ require ( dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/distribution/reference v0.6.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.0 // indirect github.com/go-openapi/jsonreference v0.21.1 // indirect github.com/go-openapi/swag v0.24.1 // indirect @@ -77,19 +70,14 @@ require ( github.com/go-openapi/swag/typeutils v0.24.0 // indirect github.com/go-openapi/swag/yamlutils v0.24.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect - github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/huandu/xstrings v1.5.0 // 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/kcp-dev/apimachinery/v2 v2.29.1-0.20251209121225-cf3c0b624983 // indirect - github.com/kcp-dev/client-go v0.28.1-0.20251209170419-79146629224a // indirect - github.com/kylelemons/godebug v1.1.0 // indirect + github.com/kcp-dev/apimachinery/v2 v2.30.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect @@ -97,32 +85,21 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.38.2 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/prometheus/client_golang v1.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.63.0 // indirect - github.com/prometheus/procfs v0.16.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/x448/float16 v0.8.4 // indirect - go.etcd.io/etcd/api/v3 v3.6.4 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect - go.etcd.io/etcd/client/v3 v3.6.4 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.66.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/sdk v1.41.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/goleak v1.3.1-0.20251210191316-2b7fd8a0d244 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect @@ -136,17 +113,11 @@ require ( gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/component-helpers v0.33.5 // indirect - k8s.io/controller-manager v0.33.5 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kubernetes v1.34.2 // 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/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect ) diff --git a/go.sum b/go.sum index 7bf2732..b58f783 100644 --- a/go.sum +++ b/go.sum @@ -8,50 +8,33 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= -github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 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= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= @@ -84,13 +67,8 @@ github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/IS github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -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.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= @@ -106,44 +84,28 @@ 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/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= -github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= -github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kcp-dev/apimachinery/v2 v2.29.1-0.20251209121225-cf3c0b624983 h1:4hCauFTFMwvIhwL9fqZ5omjeZ+vsOUNO1tLsrCeprxM= -github.com/kcp-dev/apimachinery/v2 v2.29.1-0.20251209121225-cf3c0b624983/go.mod h1:DOv0iw5tcgzFBhudwLFe2WHCLqtlgNkuO4AcqbZ4zVo= -github.com/kcp-dev/client-go v0.28.1-0.20251209170419-79146629224a h1:Fv8/Me8eSMcLScRdXTsd0wR4v1Ies8/WdXdbepOFE9s= -github.com/kcp-dev/client-go v0.28.1-0.20251209170419-79146629224a/go.mod h1:SWYbL1dVmUvLQ8DpcQq+tIH14R1i5e4wh1NTW4YaUYc= -github.com/kcp-dev/kcp v0.29.1-0.20251210093424-08fb9eb48494 h1:K5eM4sj+sdKiJ9xwpKpG4aqjNSrPpC8V4LWVHTL3pTc= -github.com/kcp-dev/kcp v0.29.1-0.20251210093424-08fb9eb48494/go.mod h1:5A7we658aPU8CIHN2ht/aejWah5aCn0Pos7UOl7Nej4= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20251209073509-71e0f2506325 h1:MmzvuhedtyTW49qG0VWf9X54OwaRslUk6KyC7vhsuI4= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20251209073509-71e0f2506325/go.mod h1:NL2CyapDmJ+5XVVY8qr6niVA3UHVF17kPl0zh6ohkVM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20251209073509-71e0f2506325 h1:YhSbN6w0bbxt0kKS7yIUivV9KBo1488HG0pPnYojP9U= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20251209073509-71e0f2506325/go.mod h1:msyjTyI8TyfhYybEkao5LA8bUrVqz1xhic5zxsfejoM= +github.com/kcp-dev/apimachinery/v2 v2.30.0 h1:bj7lVVPJj5UnQFCWhXVAKC+eNaIMKGGxpq+fE5edRU0= +github.com/kcp-dev/apimachinery/v2 v2.30.0/go.mod h1:DOv0iw5tcgzFBhudwLFe2WHCLqtlgNkuO4AcqbZ4zVo= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20251216144411-4b3495fdcb9d h1:8IVhiVfslBHX5XTRocRHn5DisT0peZANdAK+O1oeWXI= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20251216144411-4b3495fdcb9d/go.mod h1:NL2CyapDmJ+5XVVY8qr6niVA3UHVF17kPl0zh6ohkVM= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20251216144411-4b3495fdcb9d h1:Sr+/M7xxy9lfVlDbwD1o3lx/cSYujKHyThlCEnX/EVY= +github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20251216144411-4b3495fdcb9d/go.mod h1:msyjTyI8TyfhYybEkao5LA8bUrVqz1xhic5zxsfejoM= github.com/kcp-dev/logicalcluster/v3 v3.0.5 h1:JbYakokb+5Uinz09oTXomSUJVQsqfxEvU4RyHUYxHOU= github.com/kcp-dev/logicalcluster/v3 v3.0.5/go.mod h1:EWBUBxdr49fUB1cLMO4nOdBWmYifLbP1LfoL20KkXYY= -github.com/kcp-dev/multicluster-provider v0.3.2-0.20251209135920-e758bf0f4e48 h1:QgE4koFt6oU+3cfZquU467Ybv9H/HYikth5neMJeUe0= -github.com/kcp-dev/multicluster-provider v0.3.2-0.20251209135920-e758bf0f4e48/go.mod h1:4QGU39wyNztoYNatdWqbdOV6/R9ZzaIh4DdSj30dm9o= -github.com/kcp-dev/sdk v0.28.1-0.20251209130449-436a0347809b h1:hPwN5SK3L5bx4Ymeb5NeYN0lqIXd+Xt1cAr3qcSlQxU= -github.com/kcp-dev/sdk v0.28.1-0.20251209130449-436a0347809b/go.mod h1:kdlYfujcotSPrBzwtI6qrsNo4JQ+GB1o04buOfKUo2c= +github.com/kcp-dev/multicluster-provider v0.5.0 h1:G5YW2POVftsnxUfK2vo7anX5R1I3gVjjNbo/4i5ttbo= +github.com/kcp-dev/multicluster-provider v0.5.0/go.mod h1:eJohrSXqLmpjfTSFBbZMoq4Osr57UKg9ZokvhCPNmHc= +github.com/kcp-dev/sdk v0.30.0 h1:BdDiKJ7SeVfzLIxueQwbADTrH7bfZ7b5ACYSrx6P93Y= +github.com/kcp-dev/sdk v0.30.0/go.mod h1:H3PkpM33QqwPMgGOOw3dfqbQ8dF2gu4NeIsufSlS5KE= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -168,32 +130,26 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd 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.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY= -github.com/onsi/ginkgo/v2 v2.25.1/go.mod h1:ppTWQ1dh9KM/F1XgpeRqelR+zHVwV81DGRSDnFxK7Sk= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= -github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= -github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= -github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +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.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/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= @@ -224,60 +180,22 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= -go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= -go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= -go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= -go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= -go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= -go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= -go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= -go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA= -go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE= -go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= -go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= -go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= -go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.66.0 h1:w/o339tDd6Qtu3+ytwt+/jon2yjAs3Ot8Xq8pelfhSo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.66.0/go.mod h1:pdhNtM9C4H5fRdrnwO7NjxzQWhKSSxCHk/KluVqDVC0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0 h1:mq/Qcf28TWz719lE3/hMB4KkyDuLJIvgJnFGcd0kEUI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.41.0/go.mod h1:yk5LXEYhsL2htyDNJbEq7fWzNEigeEdV5xBF/Y+kAv0= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= -go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -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.1-0.20241121203838-4ff5fa6529ee h1:uOMbcH1Dmxv45VkkpZQYoerZFeDncWpjbN7ATiQOO7c= -go.uber.org/goleak v1.3.1-0.20241121203838-4ff5fa6529ee/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/goleak v1.3.1-0.20251210191316-2b7fd8a0d244 h1:OdZ8e4E9yDUGiis9x2ta/Ec5yhMAKT6ZivRvakyxC7E= +go.uber.org/goleak v1.3.1-0.20251210191316-2b7fd8a0d244/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/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/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 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= @@ -289,6 +207,8 @@ golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= 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/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= 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= @@ -327,14 +247,10 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0= gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -344,46 +260,34 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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= k8c.io/reconciler v0.5.0 h1:BHpelg1UfI/7oBFctqOq8sX6qzflXpl3SlvHe7e8wak= k8c.io/reconciler v0.5.0/go.mod h1:pT1+SVcVXJQeBJhpJBXQ5XW64QnKKeYTnVlQf0dGE0k= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= -k8s.io/component-base v0.34.2 h1:HQRqK9x2sSAsd8+R4xxRirlTjowsg6fWCPwWYeSvogQ= -k8s.io/component-base v0.34.2/go.mod h1:9xw2FHJavUHBFpiGkZoKuYZ5pdtLKe97DEByaA+hHbM= -k8s.io/component-helpers v0.33.5 h1:1LDSMzn7YTreVLPaOBJK36ase/FWi2sDpeJJvbEBO2s= -k8s.io/component-helpers v0.33.5/go.mod h1:C3HsDU2lANSLgTTgMJ0TFnG5xZrVrxR3Ss9n7Wrsw4s= -k8s.io/controller-manager v0.33.5 h1:abmssknXnhOhW533583v2SYQObD5RhYiSL7Za1rezGM= -k8s.io/controller-manager v0.33.5/go.mod h1:KuQeAlf4vI2+qj5fwPVLaDlbtrTBA/8L/LqQvI74Ow0= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.34.2 h1:91rj4MDZLyIT9KxG8J5/CcMH666Z88CF/xJQeuPfJc8= -k8s.io/kms v0.34.2/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/kubernetes v1.34.2 h1:WQdDvYJazkmkwSncgNwGvVtaCt4TYXIU3wSMRgvp3MI= -k8s.io/kubernetes v1.34.2/go.mod h1:m6pZk6a179pRo2wsTiCPORJ86iOEQmfIzUvtyEF8BwA= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= -sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= -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.22.4-beta.1 h1:0XWbDINepM9UOyLkqhG4g7BtGBFKCDvZFyPsw1vufKE= -sigs.k8s.io/multicluster-runtime v0.22.4-beta.1/go.mod h1:zSMb4mC8MAZK42l8eE1ywkeX6vjuNRenYzJ1w+GPdfI= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/multicluster-runtime v0.23.1 h1:isjVh6zBuk/U1HjYm22knRZmFsn6sFinmyvV+/4puCc= +sigs.k8s.io/multicluster-runtime v0.23.1/go.mod h1:ri1Gvx7Qehy5nis6OnTgSpJIWaf2SuorHDwF/jvbWvM= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 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/discovery/client.go b/internal/discovery/client.go index cbfe4e3..1ae128b 100644 --- a/internal/discovery/client.go +++ b/internal/discovery/client.go @@ -23,7 +23,7 @@ import ( "slices" "strings" - "github.com/kcp-dev/kcp/pkg/crdpuller" + "github.com/kcp-dev/api-syncagent/third_party/kcp/crdpuller" "k8s.io/apiextensions-apiserver/pkg/apihelpers" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" diff --git a/internal/discovery/resource_prober.go b/internal/discovery/resource_prober.go index e11567a..7fb7bdf 100644 --- a/internal/discovery/resource_prober.go +++ b/internal/discovery/resource_prober.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + "github.com/kcp-dev/api-syncagent/internal/kcp" kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -30,16 +31,23 @@ import ( ) type ResourceProber struct { - name string - config *rest.Config - client ctrlruntimeclient.Client + name string + config *rest.Config + client ctrlruntimeclient.Client + urlRewriter kcp.URLRewriterFunc } -func NewResourceProber(endpointSliceWorkspaceConfig *rest.Config, endpointSliceWorkspaceClient ctrlruntimeclient.Client, endpointSliceName string) *ResourceProber { +func NewResourceProber( + endpointSliceWorkspaceConfig *rest.Config, + endpointSliceWorkspaceClient ctrlruntimeclient.Client, + endpointSliceName string, + urlRewriter kcp.URLRewriterFunc, +) *ResourceProber { return &ResourceProber{ - name: endpointSliceName, - config: endpointSliceWorkspaceConfig, - client: endpointSliceWorkspaceClient, + name: endpointSliceName, + config: endpointSliceWorkspaceConfig, + client: endpointSliceWorkspaceClient, + urlRewriter: urlRewriter, } } @@ -75,6 +83,15 @@ func (p *ResourceProber) hasAPIThing(ctx context.Context, match matchFunc) (bool type matchFunc func(apiGroup, version, resource, kind string) bool func (p *ResourceProber) hasAPIThingInEndpoint(endpoint string, match matchFunc) (bool, error) { + if p.urlRewriter != nil { + var err error + + endpoint, err = p.urlRewriter(endpoint) + if err != nil { + return false, fmt.Errorf("failed to apply URL rewriting: %w", err) + } + } + endpointConfig := rest.CopyConfig(p.config) endpointConfig.Host = endpoint + "/clusters/*" diff --git a/internal/kcp/multicluster.go b/internal/kcp/multicluster.go index 6214e06..995a6cc 100644 --- a/internal/kcp/multicluster.go +++ b/internal/kcp/multicluster.go @@ -32,6 +32,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/cluster" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/metrics/server" @@ -54,9 +55,11 @@ type DynamicMultiClusterManager struct { runnables map[string]mcmanager.Runnable tracker *clusterTracker + + urlOverrideFunc URLRewriterFunc } -func NewDynamicMultiClusterManager(cfg *rest.Config, endpointSliceName string) (*DynamicMultiClusterManager, error) { +func NewDynamicMultiClusterManager(cfg *rest.Config, endpointSliceName string, urlRewriter URLRewriterFunc) (*DynamicMultiClusterManager, error) { scheme := runtime.NewScheme() if err := corev1.AddToScheme(scheme); err != nil { @@ -79,6 +82,26 @@ func NewDynamicMultiClusterManager(cfg *rest.Config, endpointSliceName string) ( return nil, fmt.Errorf("failed to create multicluster provider: %w", err) } + if urlRewriter != nil { + origFunc := provider.GetVWs + + provider.GetVWs = func(obj client.Object) ([]string, error) { + urls, err := origFunc(obj) + if err != nil { + return nil, err + } + + for i, url := range urls { + urls[i], err = urlRewriter(url) + if err != nil { + return nil, fmt.Errorf("failed to rewrite %q: %w", url, err) + } + } + + return urls, nil + } + } + // Setup a multiClusterManager, which will use the apiexport provider and // try to engage a dummy controller, which will just keep track of all the // engaged clusters over the entire lifetime of the process. @@ -94,9 +117,10 @@ func NewDynamicMultiClusterManager(cfg *rest.Config, endpointSliceName string) ( } dynManager := &DynamicMultiClusterManager{ - manager: multiClusterManager, - runnablesLock: sync.RWMutex{}, - runnables: map[string]mcmanager.Runnable{}, + manager: multiClusterManager, + runnablesLock: sync.RWMutex{}, + runnables: map[string]mcmanager.Runnable{}, + urlOverrideFunc: urlRewriter, } tracker := newClusterTracker(dynManager) diff --git a/internal/kcp/types.go b/internal/kcp/types.go index 0123060..971a728 100644 --- a/internal/kcp/types.go +++ b/internal/kcp/types.go @@ -23,3 +23,5 @@ const ( // the logicalcluster name (e.g. "984235jkhwfowt45"). IdentityClusterName = "cluster" ) + +type URLRewriterFunc func(url string) (string, error) diff --git a/third_party/kcp/crdpuller/discovery.go b/third_party/kcp/crdpuller/discovery.go new file mode 100644 index 0000000..b017f2f --- /dev/null +++ b/third_party/kcp/crdpuller/discovery.go @@ -0,0 +1,646 @@ +/* +Copyright 2021 The kcp Authors. + +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 crdpuller + +// We import the generic control plane scheme to provide access to the kcp control plane scheme, +// that gathers a minimal set of Kubernetes APIs without any workload-related APIs. +// +// We don't want to import, from physical clusters; resources that are already part of the control +// plane scheme. The side-effect import of the generic control plane install is to required to +// install all the required resources in the control plane scheme. +import ( + "context" + "fmt" + "strings" + + "k8s.io/apiextensions-apiserver/pkg/apihelpers" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/endpoints/openapi" + "k8s.io/client-go/discovery" + "k8s.io/client-go/discovery/cached/memory" + "k8s.io/client-go/restmapper" + "k8s.io/klog/v2" + "k8s.io/kube-openapi/pkg/util" + "k8s.io/kube-openapi/pkg/util/proto" +) + +type schemaPuller struct { + serverGroupsAndResources func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) + serverPreferredResources func() ([]*metav1.APIResourceList, error) + resourceFor func(groupResource schema.GroupResource) (schema.GroupResource, error) + getCRD func(ctx context.Context, name string) (*apiextensionsv1.CustomResourceDefinition, error) + models openapi.ModelsByGKV +} + +// NewSchemaPuller allows creating a SchemaPuller from the `Config` of +// a given Kubernetes cluster, that will be able to pull API resources +// as CRDs from the given Kubernetes cluster. +func NewSchemaPuller( + discoveryClient discovery.DiscoveryInterface, + crdClient apiextensionsv1client.ApiextensionsV1Interface, +) (*schemaPuller, error) { + openapiSchema, err := discoveryClient.OpenAPISchema() + if err != nil { + return nil, err + } + + models, err := proto.NewOpenAPIData(openapiSchema) + if err != nil { + return nil, err + } + modelsByGKV, err := openapi.GetModelsByGKV(models) + if err != nil { + return nil, err + } + + mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient)) + + return &schemaPuller{ + serverGroupsAndResources: discoveryClient.ServerGroupsAndResources, + serverPreferredResources: discoveryClient.ServerPreferredResources, + resourceFor: func(groupResource schema.GroupResource) (schema.GroupResource, error) { + gvr, err := mapper.ResourceFor(groupResource.WithVersion("")) + if err != nil { + return schema.GroupResource{}, err + } + return gvr.GroupResource(), nil + }, + getCRD: func(ctx context.Context, name string) (*apiextensionsv1.CustomResourceDefinition, error) { + return crdClient.CustomResourceDefinitions().Get(ctx, name, metav1.GetOptions{}) + }, + models: modelsByGKV, + }, nil +} + +// PullCRDs allows pulling the resources named by their plural names +// and make them available as CRDs in the output map. +// If the list of resources is empty, it will try pulling all the resources it finds. +func (sp *schemaPuller) PullCRDs(ctx context.Context, resourceNames ...string) (map[schema.GroupResource]*apiextensionsv1.CustomResourceDefinition, error) { + logger := klog.FromContext(ctx) + + pullAllResources := len(resourceNames) == 0 + resourcesToPull := sets.New[string]() + for _, resourceToPull := range resourceNames { + gr := schema.ParseGroupResource(resourceToPull) + grToPull, err := sp.resourceFor(gr) + if err != nil { + logger.Error(err, "error mapping", "resource", resourceToPull) + continue + } + resourcesToPull.Insert(grToPull.String()) + } + + _, apiResourcesLists, err := sp.serverGroupsAndResources() + if err != nil { + return nil, err + } + apiResourceNames := map[schema.GroupVersion]sets.Set[string]{} + for _, apiResourcesList := range apiResourcesLists { + gv, err := schema.ParseGroupVersion(apiResourcesList.GroupVersion) + if err != nil { + continue + } + + apiResourceNames[gv] = sets.New[string]() + for _, apiResource := range apiResourcesList.APIResources { + apiResourceNames[gv].Insert(apiResource.Name) + } + } + + crds := map[schema.GroupResource]*apiextensionsv1.CustomResourceDefinition{} + apiResourcesLists, err = sp.serverPreferredResources() + if err != nil { + return nil, err + } + for _, apiResourcesList := range apiResourcesLists { + logger := logger.WithValues("groupVersion", apiResourcesList.GroupVersion) + + gv, err := schema.ParseGroupVersion(apiResourcesList.GroupVersion) + if err != nil { + logger.Error(err, "skipping discovery: error parsing") + continue + } + + for _, apiResource := range apiResourcesList.APIResources { + groupResource := schema.GroupResource{ + Group: gv.Group, + Resource: apiResource.Name, + } + if !pullAllResources && !resourcesToPull.Has(groupResource.String()) { + continue + } + + logger := logger.WithValues("resource", apiResource.Name) + + // PATCH (xref https://github.com/kcp-dev/kcp/pull/3887) + // For the api-syncagent, we know exactly what GVR to pull + // and do not necessarily need to filter out anything. + + // if kcpscheme.Scheme.IsGroupRegistered(gv.Group) && !kcpscheme.Scheme.IsVersionRegistered(gv) { + // logger.Info("ignoring an apiVersion since it is part of the core kcp resources, but not compatible with kcp version") + // continue + // } + + gvk := gv.WithKind(apiResource.Kind) + logger = logger.WithValues("kind", apiResource.Kind) + + // if (kcpscheme.Scheme.Recognizes(gvk) || extensionsapiserver.Scheme.Recognizes(gvk)) && !resourcesToPull.Has(groupResource.String()) { + // logger.Info("ignoring a resource since it is part of the core kcp resources") + // continue + // } + + crdName := apiResource.Name + if gv.Group == "" { + crdName += ".core" + } else { + crdName += "." + gv.Group + } + + var resourceScope apiextensionsv1.ResourceScope + if apiResource.Namespaced { + resourceScope = apiextensionsv1.NamespaceScoped + } else { + resourceScope = apiextensionsv1.ClusterScoped + } + + logger = logger.WithValues("crd", crdName) + logger.Info("processing discovery") + var schemaProps apiextensionsv1.JSONSchemaProps + var additionalPrinterColumns []apiextensionsv1.CustomResourceColumnDefinition + crd, err := sp.getCRD(ctx, crdName) + if err == nil { + if apihelpers.IsCRDConditionTrue(crd, apiextensionsv1.NonStructuralSchema) { + logger.Info("non-structural schema: the resources will not be validated") + schemaProps = apiextensionsv1.JSONSchemaProps{ + Type: "object", + XPreserveUnknownFields: boolPtr(true), + } + } else { + var versionFound bool + for _, version := range crd.Spec.Versions { + if version.Name == gv.Version { + schemaProps = *version.Schema.OpenAPIV3Schema + additionalPrinterColumns = version.AdditionalPrinterColumns + versionFound = true + break + } + } + if !versionFound { + logger.Error(nil, "expected version not found in CRD") + schemaProps = apiextensionsv1.JSONSchemaProps{ + Type: "object", + XPreserveUnknownFields: boolPtr(true), + } + } + } + } else { + if !errors.IsNotFound(err) { + logger.Error(err, "error looking up CRD") + return nil, err + } + protoSchema := sp.models[gvk] + if protoSchema == nil { + logger.Info("ignoring a resource that has no OpenAPI Schema") + continue + } + swaggerSpecDefinitionName := protoSchema.GetPath().String() + + var errs []error + converter := &SchemaConverter{ + schemaProps: &schemaProps, + schemaName: swaggerSpecDefinitionName, + visited: sets.New[string](), + errors: &errs, + } + protoSchema.Accept(converter) + if len(*converter.errors) > 0 { + logger.Error(utilerrors.NewAggregate(*converter.errors), "error during the OpenAPI schema import of resource") + continue + } + } + + hasSubResource := func(subResource string) bool { + groupResourceNames := apiResourceNames[gv] + if groupResourceNames != nil { + return groupResourceNames.Has(apiResource.Name + "/" + subResource) + } + return false + } + + statusSubResource := &apiextensionsv1.CustomResourceSubresourceStatus{} + if !hasSubResource("status") { + statusSubResource = nil + } + + scaleSubResource := &apiextensionsv1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + } + if !hasSubResource("scale") { + scaleSubResource = nil + } + + publishedCRD := &apiextensionsv1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: "CustomResourceDefinition", + APIVersion: "apiextensions.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: crdName, + Labels: map[string]string{}, + Annotations: map[string]string{}, + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: gv.Group, + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: gv.Version, + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &schemaProps, + }, + Subresources: &apiextensionsv1.CustomResourceSubresources{ + Status: statusSubResource, + Scale: scaleSubResource, + }, + Served: true, + Storage: true, + }, + }, + Scope: resourceScope, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: apiResource.Name, + Kind: apiResource.Kind, + Categories: apiResource.Categories, + ShortNames: apiResource.ShortNames, + Singular: apiResource.SingularName, + }, + }, + } + if len(additionalPrinterColumns) != 0 { + publishedCRD.Spec.Versions[0].AdditionalPrinterColumns = additionalPrinterColumns + } + apiextensionsv1.SetDefaults_CustomResourceDefinition(publishedCRD) + + // In Kubernetes, to make it clear to the API consumer that APIs in *.k8s.io or *.kubernetes.io domains + // should be following all quality standards of core Kubernetes, CRDs under these domains + // are expected to go through the API Review process and so must link the API review approval PR + // in an `api-approved.kubernetes.io` annotation. + // Without this annotation, a CRD under the *.k8s.io or *.kubernetes.io domains is rejected by the API server + // + // Of course here we're simply adding already-known resources of existing physical clusters as CRDs in kcp. + // But to please this Kubernetes approval requirement, let's add the required annotation in imported CRDs + // with one of the kcp PRs that hacked Kubernetes CRD support for kcp. + if apihelpers.IsProtectedCommunityGroup(gv.Group) { + value := "https://github.com/kcp-dev/kubernetes/pull/4" + if crd != nil { + if existing := crd.ObjectMeta.Annotations[apiextensionsv1.KubeAPIApprovedAnnotation]; existing != "" { + value = existing + } + } + publishedCRD.ObjectMeta.Annotations[apiextensionsv1.KubeAPIApprovedAnnotation] = value + } + crds[groupResource] = publishedCRD + } + } + return crds, nil +} + +type SchemaConverter struct { + schemaProps *apiextensionsv1.JSONSchemaProps + schemaName string + description string + errors *[]error + visited sets.Set[string] +} + +func Convert(protoSchema proto.Schema, schemaProps *apiextensionsv1.JSONSchemaProps) []error { + swaggerSpecDefinitionName := protoSchema.GetPath().String() + + var errs []error + converter := &SchemaConverter{ + schemaProps: schemaProps, + schemaName: swaggerSpecDefinitionName, + visited: sets.New[string](), + errors: &errs, + } + protoSchema.Accept(converter) + return *converter.errors +} + +var _ proto.SchemaVisitorArbitrary = (*SchemaConverter)(nil) + +func (sc *SchemaConverter) setupDescription(schema proto.Schema) { + schemaDescription := schema.GetDescription() + inheritedDescription := sc.description + subSchemaDescription := "" + + switch typed := schema.(type) { + case *proto.Arbitrary: + case *proto.Array: + subSchemaDescription = typed.SubType.GetDescription() + case *proto.Primitive: + case *proto.Kind: + case *proto.Map: + subSchemaDescription = typed.SubType.GetDescription() + case *proto.Ref: + subSchemaDescription = typed.SubSchema().GetDescription() + } + + if inheritedDescription != "" { + sc.schemaProps.Description = inheritedDescription + } else if subSchemaDescription != "" { + sc.schemaProps.Description = subSchemaDescription + } else { + sc.schemaProps.Description = schemaDescription + } +} + +func (sc *SchemaConverter) VisitArbitrary(a *proto.Arbitrary) { + sc.setupDescription(a) + sc.schemaProps.XEmbeddedResource = true + if a.Extensions != nil { + if preserveUnknownFields, exists := a.Extensions["x-kubernetes-preserve-unknown-fields"]; exists { + boolValue := preserveUnknownFields.(bool) + sc.schemaProps.XPreserveUnknownFields = &boolValue + } + } +} + +func (sc *SchemaConverter) VisitArray(a *proto.Array) { + sc.setupDescription(a) + sc.schemaProps.Type = "array" + if len(a.Extensions) > 0 { + var kind *proto.Kind + switch subType := a.SubType.(type) { + case *proto.Ref: + refSchema := subType.SubSchema() + if aKind, isKind := refSchema.(*proto.Kind); isKind { + kind = aKind + } + case *proto.Kind: + kind = subType + } + + if val := a.Extensions["x-kubernetes-list-type"]; val != nil { + listType := val.(string) + sc.schemaProps.XListType = &listType + } else if val := a.Extensions["x-kubernetes-patch-strategy"]; val != nil && val != "" { + listType := "atomic" + if val.(string) == "merge" || strings.HasPrefix(val.(string), "merge,") || strings.HasSuffix(val.(string), ",merge") { + if kind != nil { + listType = "map" + } else { + listType = "set" + } + } + sc.schemaProps.XListType = &listType + } + if val := a.Extensions["x-kubernetes-list-map-keys"]; val != nil { + listMapKeys := val.([]interface{}) + for _, key := range listMapKeys { + sc.schemaProps.XListMapKeys = append(sc.schemaProps.XListMapKeys, key.(string)) + } + } else if val := a.Extensions["x-kubernetes-patch-merge-key"]; val != nil { + sc.schemaProps.XListMapKeys = []string{val.(string)} + if a.Extensions["x-kubernetes-patch-strategy"] == nil { + listType := "map" + sc.schemaProps.XListType = &listType + } + } + } + subtypeSchemaProps := apiextensionsv1.JSONSchemaProps{} + a.SubType.Accept(sc.SubConverter(&subtypeSchemaProps, a.SubType.GetDescription())) + + if len(subtypeSchemaProps.Properties) > 0 && len(sc.schemaProps.XListMapKeys) > 0 { + required := sets.New[string](subtypeSchemaProps.Required...) + required.Insert(sc.schemaProps.XListMapKeys...) + for fieldName, field := range subtypeSchemaProps.Properties { + if field.Default != nil { + required.Delete(fieldName) + } + } + subtypeSchemaProps.Required = sets.List[string](required) + } + + sc.schemaProps.Items = &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &subtypeSchemaProps, + } +} +func (sc *SchemaConverter) VisitMap(m *proto.Map) { + sc.setupDescription(m) + subtypeSchemaProps := apiextensionsv1.JSONSchemaProps{} + m.SubType.Accept(sc.SubConverter(&subtypeSchemaProps, m.SubType.GetDescription())) + sc.schemaProps.AdditionalProperties = &apiextensionsv1.JSONSchemaPropsOrBool{ + Schema: &subtypeSchemaProps, + Allows: true, + } + sc.schemaProps.Type = "object" +} +func (sc *SchemaConverter) VisitPrimitive(p *proto.Primitive) { + sc.setupDescription(p) + sc.schemaProps.Type = p.Type + sc.schemaProps.Format = p.Format + + if defaults, ok := knownDefaults[p.Path.String()]; ok { + sc.schemaProps.Default = defaults + } +} + +func (sc *SchemaConverter) VisitKind(k *proto.Kind) { + sc.setupDescription(k) + sc.schemaProps.Required = k.RequiredFields + sc.schemaProps.Properties = map[string]apiextensionsv1.JSONSchemaProps{} + sc.schemaProps.Type = "object" + for fieldName, field := range k.Fields { + fieldSchemaProps := apiextensionsv1.JSONSchemaProps{} + + path := field.GetPath().String() + if path == sc.schemaName+".metadata" { + fieldSchemaProps.Type = "object" + } else { + field.Accept(sc.SubConverter(&fieldSchemaProps, field.GetDescription())) + } + + sc.schemaProps.Properties[fieldName] = fieldSchemaProps + } + for extensionName, extension := range k.Extensions { + switch extensionName { + case "x-kubernetes-patch-merge-key": + sc.schemaProps.XListMapKeys = []string{extension.(string)} + case "x-kubernetes-list-map-keys": + sc.schemaProps.XListMapKeys = extension.([]string) + case "x-kubernetes-list-type": + val := extension.(string) + sc.schemaProps.XListType = &val + } + } +} + +func (sc *SchemaConverter) VisitReference(r proto.Reference) { + reference := r.Reference() // recursive CRDs are not supported + if reference == "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps" { + return + } + if sc.visited.Has(reference) { + *sc.errors = append(*sc.errors, fmt.Errorf("recursive schema are not supported: %s", reference)) + return + } + if knownSchema, schemaIsKnown := knownSchemas[reference]; schemaIsKnown { + knownSchema.DeepCopyInto(sc.schemaProps) + if sc.description != "" { + sc.schemaProps.Description = sc.description + } else { + sc.schemaProps.Description = r.GetDescription() + } + return + } + sc.setupDescription(r) + sc.visited.Insert(reference) + r.SubSchema().Accept(sc) + sc.visited.Delete(reference) +} + +func boolPtr(b bool) *bool { + return &b +} + +func (sc *SchemaConverter) SubConverter(schemaProps *apiextensionsv1.JSONSchemaProps, description string) *SchemaConverter { + return &SchemaConverter{ + schemaProps: schemaProps, + schemaName: sc.schemaName, + description: description, + errors: sc.errors, + visited: sc.visited, + } +} + +// knownPackages is a map whose content is directly borrowed from the `KnownPackages` +// map in `controller-tools `, and used to hard-code the OpenAPI V3 schema for a number +// of well-known Kubernetes types: +// https://github.com/kubernetes-sigs/controller-tools/blob/v0.5.0/pkg/crd/known_types.go#L26 +var knownPackages map[string]map[string]apiextensionsv1.JSONSchemaProps = map[string]map[string]apiextensionsv1.JSONSchemaProps{ + "k8s.io/api/core/v1": { + // Explicit defaulting for the corev1.Protocol type in lieu of https://github.com/kubernetes/enhancements/pull/1928 + "Protocol": apiextensionsv1.JSONSchemaProps{ + Type: "string", + Default: &apiextensionsv1.JSON{Raw: []byte(`"TCP"`)}, + }, + }, + + "k8s.io/apimachinery/pkg/apis/meta/v1": { + // ObjectMeta is managed by the Kubernetes API server, so no need to + // generate validation for it. + "ObjectMeta": apiextensionsv1.JSONSchemaProps{ + Type: "object", + + // CHANGE vs the OperatorSDK code: + // Add `x-preserve-unknown-fields: true` here since it is necessary when metadata is not at the top-level + // (for example in Deployment spec.template.metadata), in order not to prune unknown fields and avoid + // emptying metadata + XPreserveUnknownFields: boolPtr(true), + }, + "Time": apiextensionsv1.JSONSchemaProps{ + Type: "string", + Format: "date-time", + }, + "MicroTime": apiextensionsv1.JSONSchemaProps{ + Type: "string", + Format: "date-time", + }, + "Duration": apiextensionsv1.JSONSchemaProps{ + // TODO(directxman12): regexp validation for this (or get kube to support it as a format value) + Type: "string", + }, + "Fields": apiextensionsv1.JSONSchemaProps{ + // this is a recursive structure that can't be flattened or, for that matter, properly generated. + // so just treat it as an arbitrary map + Type: "object", + AdditionalProperties: &apiextensionsv1.JSONSchemaPropsOrBool{Allows: true}, + }, + }, + + "k8s.io/apimachinery/pkg/api/resource": { + "Quantity": apiextensionsv1.JSONSchemaProps{ + // TODO(directxman12): regexp validation for this (or get kube to support it as a format value) + XIntOrString: true, + AnyOf: []apiextensionsv1.JSONSchemaProps{ + {Type: "integer"}, + {Type: "string"}, + }, + Pattern: "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + }, + // No point in calling AddPackage, this is the sole inhabitant + }, + + "k8s.io/apimachinery/pkg/runtime": { + "RawExtension": apiextensionsv1.JSONSchemaProps{ + // TODO(directxman12): regexp validation for this (or get kube to support it as a format value) + Type: "object", + }, + }, + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured": { + "Unstructured": apiextensionsv1.JSONSchemaProps{ + Type: "object", + }, + }, + + "k8s.io/apimachinery/pkg/util/intstr": { + "IntOrString": apiextensionsv1.JSONSchemaProps{ + XIntOrString: true, + AnyOf: []apiextensionsv1.JSONSchemaProps{ + {Type: "integer"}, + {Type: "string"}, + }, + }, + // No point in calling AddPackage, this is the sole inhabitant + }, + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1": { + "JSON": apiextensionsv1.JSONSchemaProps{ + XPreserveUnknownFields: boolPtr(true), + }, + }, + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1": { + "JSON": apiextensionsv1.JSONSchemaProps{ + XPreserveUnknownFields: boolPtr(true), + }, + }, +} + +var knownSchemas map[string]apiextensionsv1.JSONSchemaProps + +func init() { + knownSchemas = map[string]apiextensionsv1.JSONSchemaProps{} + for pkgName, schemas := range knownPackages { + for typeName, s := range schemas { + schemaName := util.ToRESTFriendlyName(pkgName + "." + typeName) + knownSchemas[schemaName] = s + } + } +} + +var knownDefaults map[string]*apiextensionsv1.JSON = map[string]*apiextensionsv1.JSON{ + "io.k8s.api.core.v1.ContainerPort.protocol": {Raw: []byte(`"TCP"`)}, + "io.k8s.api.core.v1.ServicePort.protocol": {Raw: []byte(`"TCP"`)}, +}