diff --git a/.gimps.yaml b/.gimps.yaml index 6276ebe..d939b9f 100644 --- a/.gimps.yaml +++ b/.gimps.yaml @@ -21,12 +21,14 @@ sets: - 'k8s.io/**' - 'sigs.k8s.io/controller-runtime/**' - 'sigs.k8s.io/controller-tools/**' + - 'sigs.k8s.io/multicluster-runtime/**' - 'sigs.k8s.io/yaml/**' - 'github.com/kcp-dev/client-go/**' - 'github.com/kcp-dev/kubernetes/**' - name: kcp patterns: - 'github.com/kcp-dev/kcp/**' - - 'github.com/kcp-dev/multicluster-provider/**' + - 'github.com/kcp-dev/sdk/**' + - 'github.com/kcp-dev/logicalcluster/**' - 'github.com/kcp-dev/code-generator/**' - - 'sigs.k8s.io/multicluster-runtime/**' + - 'github.com/kcp-dev/multicluster-provider/**' diff --git a/.golangci.yml b/.golangci.yml index fd06ffa..03a4680 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -74,6 +74,9 @@ linters: # Controller Runtime - pkg: sigs.k8s.io/controller-runtime/pkg/client alias: ctrlruntimeclient + # kcp APIs + - pkg: github.com/kcp-dev/sdk/apis/(\w+)/(v[\w\d]+) + alias: kcp$1$2 no-unaliased: true exclusions: generated: lax diff --git a/Makefile b/Makefile index 4535b1c..9a7eba5 100644 --- a/Makefile +++ b/Makefile @@ -174,7 +174,7 @@ install-yq: @UNCOMPRESSED=true hack/uget.sh https://github.com/mikefarah/yq/releases/download/v{VERSION}/yq_{GOOS}_{GOARCH} yq $(YQ_VERSION) yq_* .PHONY: install-kcp -install-kcp: UGET_CHECKSUMS=false # do not checksum because the version regularly gets overwritten in CI jobs +install-kcp: UGET_CHECKSUMS= # do not checksum because the version regularly gets overwritten in CI jobs install-kcp: @hack/uget.sh https://github.com/kcp-dev/kcp/releases/download/v{VERSION}/kcp_{VERSION}_{GOOS}_{GOARCH}.tar.gz kcp $(KCP_VERSION) diff --git a/cmd/api-syncagent/kcp.go b/cmd/api-syncagent/kcp.go index 3d376dd..f3fdb92 100644 --- a/cmd/api-syncagent/kcp.go +++ b/cmd/api-syncagent/kcp.go @@ -22,13 +22,12 @@ import ( "fmt" "regexp" - "github.com/kcp-dev/logicalcluster/v3" - "github.com/kcp-dev/api-syncagent/internal/kcp" - kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" - kcpdevcore "github.com/kcp-dev/kcp/sdk/apis/core" - kcpdevcorev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" + kcpcore "github.com/kcp-dev/sdk/apis/core" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" @@ -39,44 +38,29 @@ import ( "sigs.k8s.io/controller-runtime/pkg/cluster" ) -// The agent has two potentially different kcp clusters: -// -// endpointCluster - this is where the source of the virtual workspace URLs -// live, i.e. where the APIExport/EndpointSlice. -// managedCluster - this is where the APIExport and APIResourceSchemas -// exist that are meant to be reconciled. -// -// The managedCluster always exists, the endpointCluster only if the workspace -// for the virtual workspace source is different from the managed cluster. - // setupEndpointKcpCluster sets up a plain, non-kcp-aware ctrl-runtime Cluster object // that is solvely used to watch whichever object holds the virtual workspace URLs, // either the APIExport or the APIExportEndpointSlice. -func setupEndpointKcpCluster(endpoint *syncEndpoint) (cluster.Cluster, error) { - // no need for a dedicated endpoint cluster - if endpoint.EndpointSlice == nil || endpoint.EndpointSlice.Cluster == endpoint.APIExport.Cluster { - return nil, nil - } - +func setupEndpointKcpCluster(endpointSlice qualifiedAPIExportEndpointSlice) (cluster.Cluster, error) { scheme := runtime.NewScheme() - if err := kcpdevv1alpha1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to register scheme %s: %w", kcpdevv1alpha1.SchemeGroupVersion, err) + if err := kcpapisv1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", kcpapisv1alpha1.SchemeGroupVersion, err) } - if err := kcpdevcorev1alpha1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to register scheme %s: %w", kcpdevcorev1alpha1.SchemeGroupVersion, err) + if err := kcpcorev1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", kcpcorev1alpha1.SchemeGroupVersion, err) } // RBAC in kcp might be very tight and might not allow to list/watch all objects; // restrict the cache's selectors accordingly so we can still make use of caching. byObject := map[ctrlruntimeclient.Object]cache.ByObject{ - &kcpdevv1alpha1.APIExportEndpointSlice{}: { - Field: fields.SelectorFromSet(fields.Set{"metadata.name": endpoint.EndpointSlice.Name}), + &kcpapisv1alpha1.APIExportEndpointSlice{}: { + Field: fields.SelectorFromSet(fields.Set{"metadata.name": endpointSlice.Name}), }, } - return cluster.New(endpoint.EndpointSlice.Config, func(o *cluster.Options) { + return cluster.New(endpointSlice.Config, func(o *cluster.Options) { o.Scheme = scheme o.Cache = cache.Options{ Scheme: scheme, @@ -87,26 +71,26 @@ func setupEndpointKcpCluster(endpoint *syncEndpoint) (cluster.Cluster, error) { // setupManagedKcpCluster sets up a plain, non-kcp-aware ctrl-runtime Cluster object // that is solvely used to manage the APIExport and APIResourceSchemas. -func setupManagedKcpCluster(endpoint *syncEndpoint) (cluster.Cluster, error) { +func setupManagedKcpCluster(apiExport qualifiedAPIExport) (cluster.Cluster, error) { scheme := runtime.NewScheme() - if err := kcpdevv1alpha1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to register scheme %s: %w", kcpdevv1alpha1.SchemeGroupVersion, err) + if err := kcpapisv1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", kcpapisv1alpha1.SchemeGroupVersion, err) } - if err := kcpdevcorev1alpha1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to register scheme %s: %w", kcpdevcorev1alpha1.SchemeGroupVersion, err) + if err := kcpcorev1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", kcpcorev1alpha1.SchemeGroupVersion, err) } // RBAC in kcp might be very tight and might not allow to list/watch all objects; // restrict the cache's selectors accordingly so we can still make use of caching. byObject := map[ctrlruntimeclient.Object]cache.ByObject{ - &kcpdevv1alpha1.APIExport{}: { - Field: fields.SelectorFromSet(fields.Set{"metadata.name": endpoint.APIExport.Name}), + &kcpapisv1alpha1.APIExport{}: { + Field: fields.SelectorFromSet(fields.Set{"metadata.name": apiExport.Name}), }, } - return cluster.New(endpoint.APIExport.Config, func(o *cluster.Options) { + return cluster.New(apiExport.Config, func(o *cluster.Options) { o.Scheme = scheme o.Cache = cache.Options{ Scheme: scheme, @@ -122,18 +106,18 @@ type qualifiedCluster struct { } type qualifiedAPIExport struct { - *kcpdevv1alpha1.APIExport + *kcpapisv1alpha1.APIExport qualifiedCluster } type qualifiedAPIExportEndpointSlice struct { - *kcpdevv1alpha1.APIExportEndpointSlice + *kcpapisv1alpha1.APIExportEndpointSlice qualifiedCluster } type syncEndpoint struct { APIExport qualifiedAPIExport - EndpointSlice *qualifiedAPIExportEndpointSlice + EndpointSlice qualifiedAPIExportEndpointSlice } // resolveSyncEndpoint takes the user provided (usually via CLI flags) APIExportEndpointSliceRef and @@ -142,14 +126,14 @@ type syncEndpoint struct { // must point to the cluster where the APIExport lives, and vice versa for the endpoint slice; // however the endpoint slice references an APIExport in potentially another cluster, and for this // case the initialRestConfig will be rewritten accordingly). -func resolveSyncEndpoint(ctx context.Context, initialRestConfig *rest.Config, endpointSliceRef string, apiExportRef string) (*syncEndpoint, error) { +func resolveSyncEndpoint(ctx context.Context, initialRestConfig *rest.Config, endpointSliceRef string) (*syncEndpoint, error) { // construct temporary, uncached client scheme := runtime.NewScheme() - if err := kcpdevcorev1alpha1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to register scheme %s: %w", kcpdevcorev1alpha1.SchemeGroupVersion, err) + if err := kcpcorev1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", kcpcorev1alpha1.SchemeGroupVersion, err) } - if err := kcpdevv1alpha1.AddToScheme(scheme); err != nil { - return nil, fmt.Errorf("failed to register scheme %s: %w", kcpdevv1alpha1.SchemeGroupVersion, err) + if err := kcpapisv1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", kcpapisv1alpha1.SchemeGroupVersion, err) } clientOpts := ctrlruntimeclient.Options{Scheme: scheme} @@ -160,58 +144,38 @@ func resolveSyncEndpoint(ctx context.Context, initialRestConfig *rest.Config, en se := &syncEndpoint{} - // When an endpoint ref is given, both the APIExportEndpointSlice and the APIExport must exist. - if endpointSliceRef != "" { - endpointSlice, err := resolveAPIExportEndpointSlice(ctx, client, endpointSliceRef) - if err != nil { - return nil, fmt.Errorf("failed to resolve APIExportEndpointSlice: %w", err) - } - endpointSlice.Config = initialRestConfig - - // find the APIExport referenced not by the user (can't: both ref parameters to this function - // are mutually exclusive), but in the APIExportEndpointSlice. - restConfig, err := retargetRestConfig(initialRestConfig, endpointSlice.Spec.APIExport.Path) - if err != nil { - return nil, fmt.Errorf("failed to re-target the given kubeconfig to cluster %q: %w", endpointSlice.Spec.APIExport.Path, err) - } - - client, err := ctrlruntimeclient.New(restConfig, clientOpts) - if err != nil { - return nil, fmt.Errorf("failed to create service reader: %w", err) - } + // First we find the APIExportEndpointSlice. + endpointSlice, err := resolveAPIExportEndpointSlice(ctx, client, endpointSliceRef) + if err != nil { + return nil, fmt.Errorf("failed to resolve APIExportEndpointSlice: %w", err) + } + endpointSlice.Config = initialRestConfig - apiExport, err := resolveAPIExport(ctx, client, endpointSlice.Spec.APIExport.Name) - if err != nil { - return nil, fmt.Errorf("failed to resolve APIExport: %w", err) - } - apiExport.Config = restConfig - - se.APIExport = apiExport - se.EndpointSlice = &endpointSlice - } else { // if an export ref is given, the endpoint slice is optional (for compat with kcp <0.28) - apiExport, err := resolveAPIExport(ctx, client, apiExportRef) - if err != nil { - return nil, fmt.Errorf("failed to resolve APIExport: %w", err) - } - apiExport.Config = initialRestConfig + // Now we find the APIExport referenced in the APIExportEndpointSlice. + restConfig, err := retargetRestConfig(initialRestConfig, endpointSlice.Spec.APIExport.Path) + if err != nil { + return nil, fmt.Errorf("failed to re-target the given kubeconfig to cluster %q: %w", endpointSlice.Spec.APIExport.Path, err) + } - se.APIExport = apiExport + client, err = ctrlruntimeclient.New(restConfig, clientOpts) + if err != nil { + return nil, fmt.Errorf("failed to create service reader: %w", err) + } - // try to find an endpoint slice in the same workspace with the same name as the APIExport - endpointSlice, err := resolveAPIExportEndpointSlice(ctx, client, apiExportRef) - if ctrlruntimeclient.IgnoreNotFound(err) != nil { - return nil, fmt.Errorf("failed to resolve APIExportEndpointSlice: %w", err) - } else if err == nil { - apiExport.Config = initialRestConfig - se.EndpointSlice = &endpointSlice - } + apiExport, err := resolveAPIExport(ctx, client, endpointSlice.Spec.APIExport.Name) + if err != nil { + return nil, fmt.Errorf("failed to resolve APIExport: %w", err) } + apiExport.Config = restConfig + + se.APIExport = apiExport + se.EndpointSlice = endpointSlice return se, nil } func resolveAPIExportEndpointSlice(ctx context.Context, client ctrlruntimeclient.Client, ref string) (qualifiedAPIExportEndpointSlice, error) { - endpointSlice := &kcpdevv1alpha1.APIExportEndpointSlice{} + endpointSlice := &kcpapisv1alpha1.APIExportEndpointSlice{} key := types.NamespacedName{Name: ref} if err := client.Get(ctx, key, endpointSlice); err != nil { return qualifiedAPIExportEndpointSlice{}, fmt.Errorf("failed to get APIExportEndpointSlice %q: %w", ref, err) @@ -232,7 +196,7 @@ func resolveAPIExportEndpointSlice(ctx context.Context, client ctrlruntimeclient } func resolveAPIExport(ctx context.Context, client ctrlruntimeclient.Client, ref string) (qualifiedAPIExport, error) { - apiExport := &kcpdevv1alpha1.APIExport{} + apiExport := &kcpapisv1alpha1.APIExport{} key := types.NamespacedName{Name: ref} if err := client.Get(ctx, key, apiExport); err != nil { return qualifiedAPIExport{}, fmt.Errorf("failed to get APIExport %q: %w", ref, err) @@ -253,13 +217,13 @@ func resolveAPIExport(ctx context.Context, client ctrlruntimeclient.Client, ref } func resolveCurrentCluster(ctx context.Context, client ctrlruntimeclient.Client) (logicalcluster.Name, logicalcluster.Path, error) { - lc := &kcpdevcorev1alpha1.LogicalCluster{} + lc := &kcpcorev1alpha1.LogicalCluster{} if err := client.Get(ctx, types.NamespacedName{Name: kcp.IdentityClusterName}, lc); err != nil { return "", logicalcluster.None, fmt.Errorf("failed to resolve current workspace: %w", err) } lcName := logicalcluster.From(lc) - lcPath := logicalcluster.NewPath(lc.Annotations[kcpdevcore.LogicalClusterPathAnnotationKey]) + lcPath := logicalcluster.NewPath(lc.Annotations[kcpcore.LogicalClusterPathAnnotationKey]) return lcName, lcPath, nil } diff --git a/cmd/api-syncagent/main.go b/cmd/api-syncagent/main.go index 708449a..ea3311e 100644 --- a/cmd/api-syncagent/main.go +++ b/cmd/api-syncagent/main.go @@ -32,12 +32,12 @@ import ( "github.com/kcp-dev/api-syncagent/internal/controller/apiexport" "github.com/kcp-dev/api-syncagent/internal/controller/apiresourceschema" "github.com/kcp-dev/api-syncagent/internal/controller/syncmanager" + "github.com/kcp-dev/api-syncagent/internal/discovery" + "github.com/kcp-dev/api-syncagent/internal/kcp" syncagentlog "github.com/kcp-dev/api-syncagent/internal/log" "github.com/kcp-dev/api-syncagent/internal/version" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" - kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" - corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/runtime" @@ -85,13 +85,11 @@ func main() { func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error { v := version.NewAppVersion() - hello := log.With("version", v.GitVersion, "name", opts.AgentName) - - if opts.APIExportEndpointSliceRef != "" { - hello = hello.With("apiexportendpointslice", opts.APIExportEndpointSliceRef) - } else { - hello = hello.With("apiexport", opts.APIExportRef) - } + hello := log.With( + "version", v.GitVersion, + "name", opts.AgentName, + "apiexportendpointslice", opts.APIExportEndpointSliceRef, + ) hello.Info("Moin, I'm the kcp Sync Agent") @@ -112,22 +110,20 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error { return fmt.Errorf("kcp kubeconfig does not point to a specific workspace") } - // We check if the APIExport/APIExportEndpointSlice exists and extract information we need to set up our kcpCluster. - endpoint, err := resolveSyncEndpoint(ctx, kcpRestConfig, opts.APIExportEndpointSliceRef, opts.APIExportRef) + // We check if the APIExportEndpointSlice exists and extract information we need to set up our kcpCluster. + endpoint, err := resolveSyncEndpoint(ctx, kcpRestConfig, opts.APIExportEndpointSliceRef) if err != nil { return fmt.Errorf("failed to resolve APIExport/EndpointSlice: %w", err) } log.Infow("Resolved APIExport", "name", endpoint.APIExport.Name, "workspace", endpoint.APIExport.Path, "logicalcluster", endpoint.APIExport.Cluster) - - if s := endpoint.EndpointSlice; s != nil { - log.Infow("Using APIExportEndpointSlice", "name", endpoint.EndpointSlice.Name, "workspace", s.Path, "logicalcluster", s.Cluster) - } + log.Infow("Using APIExportEndpointSlice", "name", endpoint.EndpointSlice.Name, "workspace", endpoint.EndpointSlice.Path, "logicalcluster", endpoint.EndpointSlice.Cluster) // init the "permanent" kcp cluster connections - // always need the managedKcpCluster - managedKcpCluster, err := setupManagedKcpCluster(endpoint) + // always need the managedKcpCluster, this is where we will manage the APIExport and + // its resource schemas. + managedKcpCluster, err := setupManagedKcpCluster(endpoint.APIExport) if err != nil { return fmt.Errorf("failed to initialize managed kcp cluster: %w", err) } @@ -138,16 +134,34 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error { return fmt.Errorf("failed to add managed kcp cluster runnable: %w", err) } - // the endpoint cluster can be nil - endpointKcpCluster, err := setupEndpointKcpCluster(endpoint) - if err != nil { - return fmt.Errorf("failed to initialize endpoint kcp cluster: %w", err) - } + endpointSliceCluster := managedKcpCluster + + // If needed, start an additional cluster for the endpoint workspace, where + // the EndpointSlice lives. + if endpoint.EndpointSlice.Cluster != endpoint.APIExport.Cluster { + endpointKcpCluster, err := setupEndpointKcpCluster(endpoint.EndpointSlice) + if err != nil { + return fmt.Errorf("failed to initialize endpoint kcp cluster: %w", err) + } - if endpointKcpCluster != nil { if err := mgr.Add(endpointKcpCluster); err != nil { return fmt.Errorf("failed to add endpoint kcp cluster runnable: %w", err) } + + endpointSliceCluster = endpointKcpCluster + } + + // Setup the magical dynamic multicluster manager. It's a dynamic version of the + // 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) + if err != nil { + return fmt.Errorf("failed to start dynamic multi cluster manager: %w", err) + } + + if err := mgr.Add(dmcm); err != nil { + return fmt.Errorf("failed to add endpoint kcp cluster runnable: %w", err) } startController := func(name string, creator func() error) error { @@ -178,20 +192,11 @@ func run(ctx context.Context, log *zap.SugaredLogger, opts *Options) error { // This controller is called "sync" because it makes the most sense to the users, even though internally the relevant // controller is the syncmanager (which in turn would start/stop the sync controllers). if err := startController("sync", func() error { - cluster := endpointKcpCluster - if cluster == nil { - cluster = managedKcpCluster - } - - var endpointSlice *kcpdevv1alpha1.APIExportEndpointSlice - if endpoint.EndpointSlice != nil { - endpointSlice = endpoint.EndpointSlice.APIExportEndpointSlice - } - - // It doesn't matter which rest config we specify, as the URL will be overwritten with the - // virtual workspace URL anyway. + // 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) - return syncmanager.Add(ctx, mgr, cluster, kcpRestConfig, log, endpoint.APIExport.APIExport, endpointSlice, opts.PublishedResourceSelector, opts.Namespace, opts.AgentName) + return syncmanager.Add(ctx, mgr, prober, dmcm, log, opts.PublishedResourceSelector, opts.Namespace, opts.AgentName) }); err != nil { return err } diff --git a/cmd/api-syncagent/options.go b/cmd/api-syncagent/options.go index 9781cb1..0ad8aba 100644 --- a/cmd/api-syncagent/options.go +++ b/cmd/api-syncagent/options.go @@ -54,15 +54,6 @@ type Options struct { // If not given, defaults to "-syncagent". AgentName string - // APIExportRef references the APIExport within a kcp workspace that this - // Sync Agent should work with by name. The APIExport has to already exist, but it must not have - // any pre-existing resource schemas configured, the agent will fill them in based on - // PublishedResources. - // - // Deprecated: Use APIExportEndpointSliceRef instead. If an APIExport is referenced, the agent - // will attempt to find and use an endpoint slice of the same name. - APIExportRef string - // APIExportEndpointSliceRef references the APIExportEndpointSlice within a kcp workspace that this // Sync Agent should work with by name. The agent will automatically manage the resource schemas // in the APIExport referenced by this endpoint slice. @@ -96,7 +87,6 @@ func (o *Options) AddFlags(flags *pflag.FlagSet) { flags.StringVar(&o.KcpKubeconfig, "kcp-kubeconfig", o.KcpKubeconfig, "kubeconfig file of kcp") flags.StringVar(&o.Namespace, "namespace", o.Namespace, "Kubernetes namespace the Sync Agent is running in") flags.StringVar(&o.AgentName, "agent-name", o.AgentName, "name of this Sync Agent, must not be changed after the first run, can be left blank to auto-generate a name") - flags.StringVar(&o.APIExportRef, "apiexport-ref", o.APIExportRef, "name of the APIExport in kcp that this Sync Agent is powering (deprecated, use --apiexportendpointslice-ref instead)") flags.StringVar(&o.APIExportEndpointSliceRef, "apiexportendpointslice-ref", o.APIExportEndpointSliceRef, "name of the APIExportEndpointSlice in kcp that this Sync Agent is powering") flags.StringVar(&o.PublishedResourceSelectorString, "published-resource-selector", o.PublishedResourceSelectorString, "restrict this Sync Agent to only process PublishedResources matching this label selector (optional)") flags.BoolVar(&o.EnableLeaderElection, "enable-leader-election", o.EnableLeaderElection, "whether to perform leader election") @@ -124,12 +114,8 @@ func (o *Options) Validate() error { } } - if len(o.APIExportRef) == 0 && len(o.APIExportEndpointSliceRef) == 0 { - errs = append(errs, errors.New("either --apiexportendpointslice-ref or --apiexport-ref is required")) - } - - if len(o.APIExportRef) != 0 && len(o.APIExportEndpointSliceRef) != 0 { - errs = append(errs, errors.New("--apiexportendpointslice-ref and --apiexport-ref are mutually exclusive")) + if len(o.APIExportEndpointSliceRef) == 0 { + errs = append(errs, errors.New("--apiexportendpointslice-ref is required")) } if len(o.KcpKubeconfig) == 0 { @@ -156,7 +142,7 @@ func (o *Options) Complete() error { errs := []error{} if len(o.AgentName) == 0 { - o.AgentName = o.APIExportRef + "-syncagent" + o.AgentName = o.APIExportEndpointSliceRef + "-syncagent" } if s := o.PublishedResourceSelectorString; len(s) > 0 { diff --git a/go.mod b/go.mod index 847ef22..fa0acb8 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.24.0 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-20250821202322-e42d885de52b - k8s.io/apiserver => github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250821202322-e42d885de52b + 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 ) require ( @@ -14,39 +14,36 @@ require ( github.com/evanphx/json-patch/v5 v5.9.11 github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 - github.com/google/cel-go v0.23.2 + github.com/google/cel-go v0.26.0 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.28.1 - github.com/kcp-dev/kcp/sdk v0.28.1 + 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.2.1-0.20250901093233-9ef571c8bd47 + 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.10 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 - go.uber.org/zap v1.27.0 + go.uber.org/zap v1.27.1 k8c.io/reconciler v0.5.0 - // Deviating from kcp's kube version (0.32) because more recent multicluster-runtime - // versions are built ontop of controller-runtime 0.21 with Kube 0.33. Ideally in - // the future we should go back to having kcp, controller-runtime and multicluster-runtime - // be in-sync again. - k8s.io/api v0.33.4 - k8s.io/apiextensions-apiserver v0.33.4 - k8s.io/apimachinery v0.33.4 - k8s.io/apiserver v0.33.4 - k8s.io/client-go v0.33.4 - k8s.io/component-base v0.33.4 - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff + 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.21.0 - sigs.k8s.io/multicluster-runtime v0.21.0-alpha.9 - sigs.k8s.io/yaml v1.4.0 + sigs.k8s.io/controller-runtime v0.22.4 + sigs.k8s.io/multicluster-runtime v0.22.4-beta.1 + sigs.k8s.io/yaml v1.6.0 ) require ( - cel.dev/expr v0.19.1 // indirect + cel.dev/expr v0.24.0 // indirect dario.cat/mergo v1.0.2 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.4.0 // indirect @@ -82,8 +79,7 @@ require ( 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.6.9 // indirect - github.com/google/uuid v1.6.0 // 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.26.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -91,14 +87,14 @@ require ( 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.0.1-0.20250728122101-adbf20db3e51 // indirect - github.com/kcp-dev/client-go v0.0.0-20250728134101-0355faa9361b // 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/mailru/easyjson v0.9.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // 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/onsi/gomega v1.38.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -112,9 +108,9 @@ require ( 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.5.21 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect - go.etcd.io/etcd/client/v3 v3.5.21 // 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.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect @@ -126,30 +122,31 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect - google.golang.org/grpc v1.71.1 // indirect + google.golang.org/grpc v1.72.1 // indirect google.golang.org/protobuf v1.36.7 // 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.32.3 // indirect - k8s.io/controller-manager v0.32.3 // 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.33.3 // 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/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) diff --git a/go.sum b/go.sum index 093b263..a96af45 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= -cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -87,17 +87,16 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ 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/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +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.23.2 h1:UdEe3CvQh3Nv+E/j9r1Y//WO0K0cSyD7/y0bzyLIMI4= -github.com/google/cel-go v0.23.2/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= -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= +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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -109,12 +108,12 @@ 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 v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= +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 v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -123,28 +122,28 @@ 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.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +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.0.1-0.20250728122101-adbf20db3e51 h1:l38RDS+VUMx9etvyaCgJIZa4nM7FaNevNubWN0kDZY4= -github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20250728122101-adbf20db3e51/go.mod h1:rF1jfvUfPjFXs+HV/LN1BtPzAz1bfjJOwVa+hAVfroQ= -github.com/kcp-dev/client-go v0.0.0-20250728134101-0355faa9361b h1:2LGrXvY9sc4l5yjKIbMZ86GEou7NyrHhA4qBPaeFfxs= -github.com/kcp-dev/client-go v0.0.0-20250728134101-0355faa9361b/go.mod h1:QdO8AaGAZPr/rIZ1iVanCM3tUOiiuX897GWv7WTByLE= -github.com/kcp-dev/kcp v0.28.1 h1:T7Ky7u9hvprkGrBnKuw0QZoP8O6TCbXqJz2Kwt6Tx+o= -github.com/kcp-dev/kcp v0.28.1/go.mod h1:q28Fx8sU/KA8kz8HGwtaqA7Iom8oR90ydoPK39jMaxo= -github.com/kcp-dev/kcp/sdk v0.28.1 h1:bTtuHVjFRjbwFEqXTPxc1J1JP2Hc3mTYqQ2xfJsi16M= -github.com/kcp-dev/kcp/sdk v0.28.1/go.mod h1:8oZpWxkoMu2TDpx5DgdIGDigByKHKkeqVMA4GiWneoI= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20250821202322-e42d885de52b h1:PsIDMrXs6N6TqdEhjoEQC2xnj6CUV9yOdc5jmlt1PPg= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiextensions-apiserver v0.0.0-20250821202322-e42d885de52b/go.mod h1:YS4BozVhJubIHAN+HRLFrXAAPd+vctFRS0u1d6DRUIM= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250821202322-e42d885de52b h1:G8dAytU6qgg9jTqeYJcsTiJkndOeOsS4/fub7JOu2A0= -github.com/kcp-dev/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20250821202322-e42d885de52b/go.mod h1:STCgTiD+xCCHsfLOPHn5sNVsyktakX/ctW3dMv3erh0= +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/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.2.1-0.20250901093233-9ef571c8bd47 h1:CPrrjZevZqUHrW/BBVvSjzSFrFnASm7srHznhTBhJ0Y= -github.com/kcp-dev/multicluster-provider v0.2.1-0.20250901093233-9ef571c8bd47/go.mod h1:RhSDOKAn/ROpSIFt140Tk2LsM7PXa3a3rov4qbqFERc= +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/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= @@ -164,8 +163,9 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx 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.25.1 h1:Fwp6crTREKM+oA6Cz4MsO8RhKQzs2/gOIVOUscMAfZY= @@ -232,22 +232,20 @@ github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chq 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.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= -go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8= -go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY= -go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc= -go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs= -go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA= -go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8= -go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY= -go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= -go.etcd.io/etcd/pkg/v3 v3.5.21 h1:jUItxeKyrDuVuWhdh0HtjUANwyuzcb7/FAeUfABmQsk= -go.etcd.io/etcd/pkg/v3 v3.5.21/go.mod h1:wpZx8Egv1g4y+N7JAsqi2zoUiBIUWznLjqJbylDjWgU= -go.etcd.io/etcd/raft/v3 v3.5.21 h1:dOmE0mT55dIUsX77TKBLq+RgyumsQuYeiRQnW/ylugk= -go.etcd.io/etcd/raft/v3 v3.5.21/go.mod h1:fmcuY5R2SNkklU4+fKVBQi2biVp5vafMrWUEj4TJ4Cs= -go.etcd.io/etcd/server/v3 v3.5.21 h1:9w0/k12majtgarGmlMVuhwXRI2ob3/d1Ik3X5TKo0yU= -go.etcd.io/etcd/server/v3 v3.5.21/go.mod h1:G1mOzdwuzKT1VRL7SqRchli/qcFrtLBTAQ4lV20sXXo= +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.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= @@ -276,65 +274,65 @@ go.uber.org/goleak v1.3.1-0.20241121203838-4ff5fa6529ee h1:uOMbcH1Dmxv45VkkpZQYo go.uber.org/goleak v1.3.1-0.20241121203838-4ff5fa6529ee/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.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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/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/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= +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/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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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 v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= -google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -351,40 +349,39 @@ 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.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= -k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= -k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= -k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= -k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= -k8s.io/component-base v0.33.4 h1:Jvb/aw/tl3pfgnJ0E0qPuYLT0NwdYs1VXXYQmSuxJGY= -k8s.io/component-base v0.33.4/go.mod h1:567TeSdixWW2Xb1yYUQ7qk5Docp2kNznKL87eygY8Rc= -k8s.io/component-helpers v0.32.3 h1:9veHpOGTPLluqU4hAu5IPOwkOIZiGAJUhHndfVc5FT4= -k8s.io/component-helpers v0.32.3/go.mod h1:utTBXk8lhkJewBKNuNf32Xl3KT/0VV19DmiXU/SV4Ao= -k8s.io/controller-manager v0.32.3 h1:jBxZnQ24k6IMeWLyxWZmpa3QVS7ww+osAIzaUY/jqyc= -k8s.io/controller-manager v0.32.3/go.mod h1:out1L3DZjE/p7JG0MoMMIaQGWIkt3c+pKaswqSHgKsI= +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/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kms v0.32.3 h1:HhHw5+pRCzEJp3oFFJ1q5W2N6gAI7YkUg4ay4Z0dgwM= -k8s.io/kms v0.32.3/go.mod h1:Bk2evz/Yvk0oVrvm4MvZbgq8BD34Ksxs2SRHn4/UiOM= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubernetes v1.33.3 h1:dBx5Z2ZhR8kNzAwCoCz4j1niUbUrNUDVxeSj4/Ienu0= -k8s.io/kubernetes v1.33.3/go.mod h1:nrt8sldmckKz2fCZhgRX3SKfS2e+CzXATPv6ITNkU00= +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.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +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.21.0-alpha.9 h1:baonM4f081WWct3U7O4EfqrxcUGtmCrFDbsT1FQ8xlo= -sigs.k8s.io/multicluster-runtime v0.21.0-alpha.9/go.mod h1:CpBzLMLQKdm+UCchd2FiGPiDdCxM5dgCCPKuaQ6Fsv0= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +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= 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/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +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/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/hack/reconciling.yaml b/hack/reconciling.yaml index 321400d..930f81f 100644 --- a/hack/reconciling.yaml +++ b/hack/reconciling.yaml @@ -19,5 +19,5 @@ package: reconciling boilerplate: hack/boilerplate/generated/boilerplate.go.txt resourceTypes: # kcp-dev/v1alpha1 - - { package: github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1, importAlias: kcpdevv1alpha1, resourceName: APIExport } - - { package: github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1, importAlias: kcpdevv1alpha1, resourceName: APIResourceSchema } + - { package: github.com/kcp-dev/sdk/apis/apis/v1alpha1, importAlias: kcpdevv1alpha1, resourceName: APIExport } + - { package: github.com/kcp-dev/sdk/apis/apis/v1alpha1, importAlias: kcpdevv1alpha1, resourceName: APIResourceSchema } diff --git a/internal/controller/apiexport/controller.go b/internal/controller/apiexport/controller.go index eda3a92..c4e581e 100644 --- a/internal/controller/apiexport/controller.go +++ b/internal/controller/apiexport/controller.go @@ -20,7 +20,6 @@ import ( "context" "fmt" - "github.com/kcp-dev/logicalcluster/v3" "go.uber.org/zap" "github.com/kcp-dev/api-syncagent/internal/controllerutil" @@ -31,7 +30,8 @@ import ( "github.com/kcp-dev/api-syncagent/internal/resources/reconciling" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" - kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" @@ -109,7 +109,7 @@ func Add( // Watch for changes to APIExport on the kcp side to start/restart the actual syncing controllers; // the cache is already restricted by a fieldSelector in the main.go to respect the RBC restrictions, // so there is no need here to add an additional filter. - WatchesRawSource(source.Kind(kcpCluster.GetCache(), &kcpdevv1alpha1.APIExport{}, controllerutil.EnqueueConst[*kcpdevv1alpha1.APIExport]("dummy"))). + WatchesRawSource(source.Kind(kcpCluster.GetCache(), &kcpapisv1alpha1.APIExport{}, controllerutil.EnqueueConst[*kcpapisv1alpha1.APIExport]("dummy"))). // Watch for changes to PublishedResources on the local service cluster Watches(&syncagentv1alpha1.PublishedResource{}, controllerutil.EnqueueConst[ctrlruntimeclient.Object]("dummy"), builder.WithPredicates(predicateutil.ByLabels(prFilter), hasARS)). Build(reconciler) @@ -120,7 +120,7 @@ func Add( func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) { r.log.Debug("Processing") - apiExport := &kcpdevv1alpha1.APIExport{} + apiExport := &kcpapisv1alpha1.APIExport{} if err := r.kcpClient.Get(ctx, types.NamespacedName{Name: r.apiExportName}, apiExport); err != nil { return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err) } @@ -128,7 +128,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, request reconcile.Request) ( return reconcile.Result{}, r.reconcile(ctx, apiExport) } -func (r *Reconciler) reconcile(ctx context.Context, apiExport *kcpdevv1alpha1.APIExport) error { +func (r *Reconciler) reconcile(ctx context.Context, apiExport *kcpapisv1alpha1.APIExport) error { // find all PublishedResources; we keep those that are not yet converted into ARS, // just to reduce the amount of re-reconciles when the agent processes a number of PRs in a row // and would constantly update the APIExport; instead we rely on kcp to handle the eventual diff --git a/internal/controller/apiexport/reconciler.go b/internal/controller/apiexport/reconciler.go index abad6ab..f462341 100644 --- a/internal/controller/apiexport/reconciler.go +++ b/internal/controller/apiexport/reconciler.go @@ -24,7 +24,7 @@ import ( "github.com/kcp-dev/api-syncagent/internal/resources/reconciling" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" - kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -61,7 +61,7 @@ func (r *Reconciler) createAPIExportReconciler( recorder record.EventRecorder, ) reconciling.NamedAPIExportReconcilerFactory { return func() (string, reconciling.APIExportReconciler) { - return apiExportName, func(existing *kcpdevv1alpha1.APIExport) (*kcpdevv1alpha1.APIExport, error) { + return apiExportName, func(existing *kcpapisv1alpha1.APIExport) (*kcpapisv1alpha1.APIExport, error) { if existing.Annotations == nil { existing.Annotations = map[string]string{} } @@ -102,8 +102,8 @@ func (r *Reconciler) createAPIExportReconciler( // add our missing claims for _, claimed := range claimsToAdd { - existing.Spec.PermissionClaims = append(existing.Spec.PermissionClaims, kcpdevv1alpha1.PermissionClaim{ - GroupResource: kcpdevv1alpha1.GroupResource{ + existing.Spec.PermissionClaims = append(existing.Spec.PermissionClaims, kcpapisv1alpha1.PermissionClaim{ + GroupResource: kcpapisv1alpha1.GroupResource{ Group: claimed.Group, Resource: claimed.Resource, }, @@ -121,7 +121,7 @@ func (r *Reconciler) createAPIExportReconciler( } // prevent reconcile loops by ensuring a stable order - slices.SortFunc(existing.Spec.PermissionClaims, func(a, b kcpdevv1alpha1.PermissionClaim) int { + slices.SortFunc(existing.Spec.PermissionClaims, func(a, b kcpapisv1alpha1.PermissionClaim) int { if a.Group != b.Group { return strings.Compare(a.Group, b.Group) } diff --git a/internal/controller/apiresourceschema/controller.go b/internal/controller/apiresourceschema/controller.go index bacb4ce..3b4c981 100644 --- a/internal/controller/apiresourceschema/controller.go +++ b/internal/controller/apiresourceschema/controller.go @@ -21,7 +21,6 @@ import ( "fmt" "reflect" - "github.com/kcp-dev/logicalcluster/v3" "go.uber.org/zap" "github.com/kcp-dev/api-syncagent/internal/controllerutil/predicate" @@ -30,7 +29,8 @@ import ( "github.com/kcp-dev/api-syncagent/internal/projection" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" - kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -159,7 +159,7 @@ func (r *Reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, pubR arsName := kcp.GetAPIResourceSchemaName(projectedCRD) // ensure ARS exists (don't try to reconcile it, it's basically entirely immutable) - ars := &kcpdevv1alpha1.APIResourceSchema{} + ars := &kcpapisv1alpha1.APIResourceSchema{} err = r.kcpClient.Get(ctx, types.NamespacedName{Name: arsName}, ars, &ctrlruntimeclient.GetOptions{}) if apierrors.IsNotFound(err) { diff --git a/internal/controller/sync/controller.go b/internal/controller/sync/controller.go index 86eae42..509d8f4 100644 --- a/internal/controller/sync/controller.go +++ b/internal/controller/sync/controller.go @@ -22,7 +22,6 @@ import ( "time" "github.com/go-logr/zapr" - "github.com/kcp-dev/logicalcluster/v3" "go.uber.org/zap" "github.com/kcp-dev/api-syncagent/internal/discovery" @@ -31,13 +30,9 @@ import ( "github.com/kcp-dev/api-syncagent/internal/sync" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" - kcpcore "github.com/kcp-dev/kcp/sdk/apis/core" - kcpdevcorev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" - mccontroller "sigs.k8s.io/multicluster-runtime/pkg/controller" - mchandler "sigs.k8s.io/multicluster-runtime/pkg/handler" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" - mcsource "sigs.k8s.io/multicluster-runtime/pkg/source" + "github.com/kcp-dev/logicalcluster/v3" + kcpcore "github.com/kcp-dev/sdk/apis/core" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -52,6 +47,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" + mccontroller "sigs.k8s.io/multicluster-runtime/pkg/controller" + mchandler "sigs.k8s.io/multicluster-runtime/pkg/handler" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + mcsource "sigs.k8s.io/multicluster-runtime/pkg/source" ) const ( @@ -134,12 +134,12 @@ func Create( // The manager parameter is mostly unused and will be removed in future CR versions. c, err := mccontroller.NewUnmanaged(ControllerName, remoteManager, ctrlOptions) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to instantiate new controller: %w", err) } // watch the target resource in the virtual workspace if err := c.MultiClusterWatch(mcsource.TypedKind(remoteDummy, mchandler.TypedEnqueueRequestForObject[*unstructured.Unstructured]())); err != nil { - return nil, err + return nil, fmt.Errorf("failed to setup remote-side watch: %w", err) } // watch the source resource in the local cluster, but enqueue the origin remote object @@ -158,7 +158,7 @@ func Create( }) if err := c.Watch(source.TypedKind(localManager.GetCache(), localDummy, enqueueRemoteObjForLocalObj, nameFilter)); err != nil { - return nil, err + return nil, fmt.Errorf("failed to setup local-side watch: %w", err) } log.Info("Done setting up unmanaged controller.") @@ -215,8 +215,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, request mcreconcile.Request) // if desired, fetch the workspace path as well (some downstream service providers might make use of it, // but since it requires an additional permission claim, it's optional) if r.pubRes.Spec.EnableWorkspacePaths { - lc := &kcpdevcorev1alpha1.LogicalCluster{} - if err := vwClient.Get(ctx, types.NamespacedName{Name: kcpdevcorev1alpha1.LogicalClusterName}, lc); err != nil { + lc := &kcpcorev1alpha1.LogicalCluster{} + if err := vwClient.Get(ctx, types.NamespacedName{Name: kcpcorev1alpha1.LogicalClusterName}, lc); err != nil { recorder.Event(remoteObj, corev1.EventTypeWarning, "ReconcilingError", "Failed to retrieve workspace path, cannot process object.") return reconcile.Result{}, fmt.Errorf("failed to retrieve remote logicalcluster: %w", err) } diff --git a/internal/controller/syncmanager/controller.go b/internal/controller/syncmanager/controller.go index 8f5f966..0308501 100644 --- a/internal/controller/syncmanager/controller.go +++ b/internal/controller/syncmanager/controller.go @@ -20,37 +20,25 @@ import ( "context" "errors" "fmt" - "slices" "sync" + "time" "go.uber.org/zap" controllersync "github.com/kcp-dev/api-syncagent/internal/controller/sync" - "github.com/kcp-dev/api-syncagent/internal/controllerutil" "github.com/kcp-dev/api-syncagent/internal/controllerutil/predicate" "github.com/kcp-dev/api-syncagent/internal/discovery" + "github.com/kcp-dev/api-syncagent/internal/kcp" + "github.com/kcp-dev/api-syncagent/internal/projection" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" - kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" - kcpcorev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" - apiexportprovider "github.com/kcp-dev/multicluster-provider/apiexport" - mccontroller "sigs.k8s.io/multicluster-runtime/pkg/controller" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/builder" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/cluster" - "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/controller-runtime/pkg/source" ) const ( @@ -60,6 +48,8 @@ const ( numSyncWorkers = 4 ) +type ClusterProviderFunc func() []string + type Reconciler struct { // choose to break good practice of never storing a context in a struct, // and instead opt to use the app's root context for the dynamically @@ -68,62 +58,29 @@ type Reconciler struct { ctx context.Context localManager manager.Manager - kcpCluster cluster.Cluster - kcpRestConfig *rest.Config + dmcm *kcp.DynamicMultiClusterManager log *zap.SugaredLogger recorder record.EventRecorder discoveryClient *discovery.Client + resourceProber *discovery.ResourceProber prFilter labels.Selector stateNamespace string agentName string - // endpointSlice is preferred over apiExport - apiExport *kcpapisv1alpha1.APIExport - endpointSlice *kcpapisv1alpha1.APIExportEndpointSlice - - // URL for which the current vwCluster instance has been created - vwURL string - - // A multi-cluster Manager representing the virtual workspace cluster; this manager will - // not handle the individual controllers' lifecycle, because their lifecycle depends on - // PublishedResources, not the set of workspaces/clusters in the APIExport's virtual workspace. - // This manager is stopped and recreated whenever the APIExport's URL changes. - vwManager mcmanager.Manager - vwManagerCtx context.Context - vwManagerCancel context.CancelFunc - - // The provider based on the APIExport; like the vwManager, this is stopped and recreated - // whenever the APIExport's URL changes. - providerOnce sync.Once - vwProvider *apiexportprovider.Provider - - syncWorkersLock sync.RWMutex + syncCancelsLock sync.RWMutex // A map of sync controllers, one for each PublishedResource, using their // UIDs and resourceVersion as the map keys; using the version ensures that // when a PR changes, the old controller is orphaned and will be shut down. - syncWorkers map[string]syncWorker - - clustersLock sync.RWMutex - // A map of clusters that have been engaged with the shim layer. Since this - // reconciler dynamically starts and stops controllers, we need to keep track - // of clusters and engage them with sync controllers started at a later point in time. - clusters map[string]engagedCluster -} - -type syncWorker struct { - controller mccontroller.Controller - cancel context.CancelFunc + syncCancels map[string]context.CancelCauseFunc } // Add creates a new controller and adds it to the given manager. func Add( ctx context.Context, localManager manager.Manager, - kcpCluster cluster.Cluster, - kcpRestConfig *rest.Config, + resourceProber *discovery.ResourceProber, + dmcm *kcp.DynamicMultiClusterManager, log *zap.SugaredLogger, - apiExport *kcpapisv1alpha1.APIExport, - endpointSlice *kcpapisv1alpha1.APIExportEndpointSlice, prFilter labels.Selector, stateNamespace string, agentName string, @@ -136,43 +93,26 @@ func Add( reconciler := &Reconciler{ ctx: ctx, localManager: localManager, - apiExport: apiExport, - endpointSlice: endpointSlice, - kcpCluster: kcpCluster, - kcpRestConfig: kcpRestConfig, + dmcm: dmcm, log: log, recorder: localManager.GetEventRecorderFor(ControllerName), discoveryClient: discoveryClient, prFilter: prFilter, stateNamespace: stateNamespace, agentName: agentName, - - providerOnce: sync.Once{}, - - syncWorkersLock: sync.RWMutex{}, - syncWorkers: map[string]syncWorker{}, - - clustersLock: sync.RWMutex{}, - clusters: make(map[string]engagedCluster), + resourceProber: resourceProber, + syncCancelsLock: sync.RWMutex{}, + syncCancels: map[string]context.CancelCauseFunc{}, } - bldr := builder.ControllerManagedBy(localManager). + bldr := builder. + ControllerManagedBy(localManager). Named(ControllerName). - WithOptions(controller.Options{ - // this controller is meant to control others, so we only want 1 thread - MaxConcurrentReconciles: 1, - }). - // Watch for changes to the PublishedResources - Watches(&syncagentv1alpha1.PublishedResource{}, controllerutil.EnqueueConst[ctrlruntimeclient.Object]("dummy"), builder.WithPredicates(predicate.ByLabels(prFilter))) - - // Watch for changes to APIExport/EndpointSlice on the kcp side to start/restart the actual syncing controllers; - // the cache is already restricted by a fieldSelector in the main.go to respect the RBAC restrictions, - // so there is no need here to add an additional filter. - if endpointSlice != nil { - bldr.WatchesRawSource(source.Kind(kcpCluster.GetCache(), &kcpapisv1alpha1.APIExportEndpointSlice{}, controllerutil.EnqueueConst[*kcpapisv1alpha1.APIExportEndpointSlice]("dummy"))) - } else { - bldr.WatchesRawSource(source.Kind(kcpCluster.GetCache(), &kcpapisv1alpha1.APIExport{}, controllerutil.EnqueueConst[*kcpapisv1alpha1.APIExport]("dummy"))) - } + Watches( + &syncagentv1alpha1.PublishedResource{}, + &handler.TypedEnqueueRequestForObject[ctrlruntimeclient.Object]{}, + builder.WithPredicates(predicate.ByLabels(prFilter)), + ) _, err = bldr.Build(reconciler) @@ -181,236 +121,119 @@ func Add( func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.Named(ControllerName) - log.Debug("Processing") + log.With("request", req.Name).Debug("Processing") - var err error - - if r.endpointSlice != nil { - if err := r.kcpCluster.GetClient().Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(r.endpointSlice), r.endpointSlice); err != nil { - return reconcile.Result{}, fmt.Errorf("failed to retrieve APIExportEndpointSlice: %w", err) - } + pubRes := &syncagentv1alpha1.PublishedResource{} + if err := r.localManager.GetClient().Get(ctx, req.NamespacedName, pubRes); err != nil { + return reconcile.Result{}, ctrlruntimeclient.IgnoreNotFound(err) + } - urls := r.endpointSlice.Status.APIExportEndpoints + var ( + err error + result reconcile.Result + ) - if len(urls) == 0 { - // the virtual workspace is not ready yet - log.Warn("APIExportEndpointSlice has no URLs.") - } else { - err = r.reconcile(ctx, log, urls[0].URL) - } + if pubRes.DeletionTimestamp != nil || pubRes.Status.ResourceSchemaName == "" || !isSyncEnabled(pubRes) { + // For PubRes that have not yet been processed, have their sync disabled or are in + // deletion, we cleanup and stop any potentially running sync controller. + err = r.cleanupController(log, pubRes) } else { - if err := r.kcpCluster.GetClient().Get(ctx, ctrlruntimeclient.ObjectKeyFromObject(r.apiExport), r.apiExport); err != nil { - return reconcile.Result{}, fmt.Errorf("failed to retrieve APIExport: %w", err) - } - - //nolint:staticcheck - urls := r.apiExport.Status.VirtualWorkspaces - - if len(urls) == 0 { - // the virtual workspace is not ready yet - log.Warn("APIExport has no virtual workspace URLs.") - } else { - err = r.reconcile(ctx, log, urls[0].URL) - } + // Otherwise, ensure a sync controller is running. + result, err = r.ensureSyncController(ctx, log, pubRes) } - return reconcile.Result{}, err + return result, err } -func (r *Reconciler) reconcile(ctx context.Context, log *zap.SugaredLogger, vwURL string) error { - // if the VW URL changed, stop the manager and all sync controllers - if r.vwURL != "" && vwURL != r.vwURL { - r.shutdown(log) - } - - // if kcp had a hiccup and wrote a status without an actual URL - if vwURL == "" { - return nil - } - - // make sure we have a running manager object for the virtual workspace - if err := r.ensureManager(log, vwURL); err != nil { - return fmt.Errorf("failed to ensure virtual workspace manager: %w", err) - } - - // find all PublishedResources - pubResList := &syncagentv1alpha1.PublishedResourceList{} - if err := r.localManager.GetClient().List(ctx, pubResList, &ctrlruntimeclient.ListOptions{ - LabelSelector: r.prFilter, - }); err != nil { - return fmt.Errorf("failed to list PublishedResources: %w", err) - } - - // Filter out those that have not been processed into APIResourceSchemas yet; starting - // sync controllers too early, before the schemes are available, will make the watches - // not work properly. - // Also remove those PRs that have sync disabled. - pubResources := slices.DeleteFunc(pubResList.Items, func(pr syncagentv1alpha1.PublishedResource) bool { - return pr.Status.ResourceSchemaName == "" || !isSyncEnabled(&pr) - }) - - // make sure that for every PublishedResource, a matching sync controller exists - if err := r.ensureSyncControllers(ctx, log, pubResources); err != nil { - return fmt.Errorf("failed to ensure sync controllers: %w", err) - } - - return nil -} +func (r *Reconciler) ensureSyncController(ctx context.Context, log *zap.SugaredLogger, pubRes *syncagentv1alpha1.PublishedResource) (reconcile.Result, error) { + key := getPublishedResourceKey(pubRes) -func (r *Reconciler) ensureManager(log *zap.SugaredLogger, vwURL string) error { - if r.vwManagerCtx == nil { - // Use the global app context so this provider is independent of the reconcile - // context, which might get cancelled right after Reconcile() is done. - r.vwManagerCtx, r.vwManagerCancel = context.WithCancel(r.ctx) + // controller already exists + if _, exists := r.syncCancels[key]; exists { + return reconcile.Result{}, nil } - vwConfig := rest.CopyConfig(r.kcpRestConfig) - vwConfig.Host = vwURL + prlog := log.With("prkey", key, "name", pubRes.Name) - scheme := runtime.NewScheme() - - if err := corev1.AddToScheme(scheme); err != nil { - return fmt.Errorf("failed to register scheme %s: %w", corev1.SchemeGroupVersion, err) - } - - if err := kcpapisv1alpha1.AddToScheme(scheme); err != nil { - return fmt.Errorf("failed to register scheme %s: %w", kcpapisv1alpha1.SchemeGroupVersion, err) - } - - if err := kcpcorev1alpha1.AddToScheme(scheme); err != nil { - return fmt.Errorf("failed to register scheme %s: %w", kcpcorev1alpha1.SchemeGroupVersion, err) + // Multicluster-runtime *hates* it when you create a controller that watches + // a resource that doesn't exist yet. So before we can proceed, we need to make + // sure the synced resource is available (there is no good condition to + // check anywhere, so a deep service discovery is required). + resourceBound, err := r.checkResourceIsBoundEverywhere(ctx, log, pubRes) + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to probe for existence of resource in virtual workspace: %w", err) + } + + if !resourceBound { + prlog.Info("Not all required resources are yet available, re-trying controller creation in a moment") + // TODO: Or return an error to have it exponentially back off? + return reconcile.Result{RequeueAfter: 1 * time.Second}, nil + } + + // Use the global app context so this provider is independent of the reconcile + // context, which might get cancelled right after Reconcile() is done. + ctrlCtx, ctrlCancel := context.WithCancelCause(r.ctx) + + prlog.Info("Creating new sync controller…") + + // create the sync controller; + // use the reconciler's log without any additional reconciling context + syncController, err := controllersync.Create( + // This can be the reconciling context, as it's only used to find the target CRD during setup; + // this context *must not* be stored in the sync controller! + ctx, + r.localManager, + r.dmcm.GetManager(), + pubRes, + r.discoveryClient, + r.stateNamespace, + r.agentName, + r.log, + numSyncWorkers, + ) + if err != nil { + ctrlCancel(errors.New("failed to create sync controller")) + return reconcile.Result{}, fmt.Errorf("failed to create sync controller: %w", err) } - if r.vwProvider == nil { - log.Debug("Setting up APIExport provider…") - - provider, err := apiexportprovider.New(vwConfig, apiexportprovider.Options{ - Scheme: scheme, - }) - if err != nil { - return fmt.Errorf("failed to init apiexport provider: %w", err) - } + r.syncCancels[key] = ctrlCancel - r.vwProvider = provider + // time to start the controller; this will spawn a new goroutine if + // successful; the new controller will be pre-seeded with all knonwn + // (engaged) clusters by the DMCM. + if err = r.dmcm.StartController(ctrlCtx, log.With("prkey", key), syncController); err != nil { + ctrlCancel(errors.New("failed to start sync controller")) + return reconcile.Result{}, fmt.Errorf("failed to start sync controller: %w", err) } - if r.vwManager == nil { - log.Debug("Setting up virtual workspace manager…") - - manager, err := mcmanager.New(vwConfig, r.vwProvider, manager.Options{ - Scheme: scheme, - LeaderElection: false, - Metrics: server.Options{ - BindAddress: "0", - }, - }) - if err != nil { - return fmt.Errorf("failed to initialize cluster: %w", err) - } - - // Make sure the vwManager can Engage() on the controller, even though we - // start and stop them outside the control of the manager. This shim will - // ensure Engage() calls are handed to the underlying sync controller as - // as long as the controller is running. - if err := manager.Add(&controllerShim{reconciler: r}); err != nil { - return fmt.Errorf("failed to initialize cluster: %w", err) - } - - // use the app's root context as the base, not the reconciling context, which - // might get cancelled after Reconcile() is done; - // likewise use the reconciler's log without any additional reconciling context - go func() { - if err := manager.Start(r.vwManagerCtx); err != nil { - log.Fatalw("Failed to start manager.", zap.Error(err)) - } - }() - - log.Debug("Virtual workspace cluster setup completed.") - - r.vwURL = vwURL - r.vwManager = manager - } - - r.providerOnce.Do(func() { - log.Debug("Starting virtual workspace provider…") - // start the provider - go func() { - // Use the global app context so this provider is independent of the reconcile - // context, which might get cancelled right after Reconcile() is done. - if err := r.vwProvider.Run(r.vwManagerCtx, r.vwManager); err != nil { - log.Fatalw("Failed to start apiexport provider", zap.Error(err)) - } - }() - }) - - return nil -} - -type engagedCluster struct { - ctx context.Context - cl cluster.Cluster + return reconcile.Result{}, nil } -type controllerShim struct { - reconciler *Reconciler -} +func (r *Reconciler) cleanupController(log *zap.SugaredLogger, pubRes *syncagentv1alpha1.PublishedResource) error { + key := getPublishedResourceKey(pubRes) + log.Infow("Stopping sync controller…", "prkey", key) -func (s *controllerShim) Engage(ctx context.Context, clusterName string, cl cluster.Cluster) error { - if _, ok := s.reconciler.clusters[clusterName]; !ok { - s.reconciler.clustersLock.Lock() - s.reconciler.clusters[clusterName] = engagedCluster{ctx: ctx, cl: cl} - s.reconciler.clustersLock.Unlock() - - // start a goroutine to make sure we remove the cluster when the context is done - go func() { - <-ctx.Done() - s.reconciler.clustersLock.Lock() - delete(s.reconciler.clusters, clusterName) - s.reconciler.clustersLock.Unlock() - }() - } + r.syncCancelsLock.Lock() + defer r.syncCancelsLock.Unlock() - s.reconciler.syncWorkersLock.RLock() - defer s.reconciler.syncWorkersLock.RUnlock() - for _, worker := range s.reconciler.syncWorkers { - if err := worker.controller.Engage(ctx, clusterName, cl); err != nil { - return err - } + cancel, ok := r.syncCancels[key] + if ok { + cancel(errors.New("controller is no longer needed")) + delete(r.syncCancels, key) } return nil } -func (s *controllerShim) Start(_ context.Context) error { - // NOP, controllers are started outside the control of the manager. - return nil -} - -// shutdown will cancel the current context and thereby stop the manager and all -// sync controllers at the same time. -func (r *Reconciler) shutdown(log *zap.SugaredLogger) { - log.Debug("Shutting down existing manager…") - - if r.vwManagerCancel != nil { - r.vwManagerCancel() +func (r *Reconciler) checkResourceIsBoundEverywhere(ctx context.Context, log *zap.SugaredLogger, pubRes *syncagentv1alpha1.PublishedResource) (bool, error) { + projectedGVK, err := projection.ProjectPublishedResourceGVK(ctx, r.discoveryClient, pubRes) + if err != nil { + return false, fmt.Errorf("failed to determine projected primary GVK: %w", err) } - r.vwProvider = nil - r.vwManager = nil - r.vwManagerCtx = nil - r.vwManagerCancel = nil - r.vwURL = "" - r.providerOnce = sync.Once{} - - r.clustersLock.Lock() - r.clusters = make(map[string]engagedCluster) - r.clustersLock.Unlock() - - // Free all workers; since their contexts are based on the manager's context, - // they have also been cancelled already above. - r.syncWorkersLock.Lock() - r.syncWorkers = make(map[string]syncWorker) - r.syncWorkersLock.Unlock() + return r.resourceProber.HasGVK(ctx, projectedGVK) + + // TODO: If the syncagent ever watches related resources, it also needs to check their GVRs here. } func getPublishedResourceKey(pr *syncagentv1alpha1.PublishedResource) string { @@ -420,91 +243,3 @@ func getPublishedResourceKey(pr *syncagentv1alpha1.PublishedResource) string { func isSyncEnabled(pr *syncagentv1alpha1.PublishedResource) bool { return pr.Spec.Synchronization == nil || pr.Spec.Synchronization.Enabled } - -func (r *Reconciler) ensureSyncControllers(ctx context.Context, log *zap.SugaredLogger, publishedResources []syncagentv1alpha1.PublishedResource) error { - requiredWorkers := sets.New[string]() - for _, pr := range publishedResources { - requiredWorkers.Insert(getPublishedResourceKey(&pr)) - } - - // stop controllers that are no longer needed - for key, worker := range r.syncWorkers { - if requiredWorkers.Has(key) { - continue - } - - log.Infow("Stopping sync controller…", "key", key) - - worker.cancel() - - r.syncWorkersLock.Lock() - delete(r.syncWorkers, key) - r.syncWorkersLock.Unlock() - } - - // start missing controllers - for _, pubRes := range publishedResources { - key := getPublishedResourceKey(&pubRes) - - // controller already exists - if _, exists := r.syncWorkers[key]; exists { - continue - } - - prlog := log.With("key", key, "name", pubRes.Name) - ctrlCtx, ctrlCancel := context.WithCancel(r.vwManagerCtx) - - prlog.Info("Creating new sync controller…") - - // create the sync controller; - // use the reconciler's log without any additional reconciling context - syncController, err := controllersync.Create( - // This can be the reconciling context, as it's only used to find the target CRD during setup; - // this context *must not* be stored in the sync controller! - ctx, - r.localManager, - r.vwManager, - &pubRes, - r.discoveryClient, - r.stateNamespace, - r.agentName, - r.log, - numSyncWorkers, - ) - if err != nil { - ctrlCancel() - return fmt.Errorf("failed to create sync controller: %w", err) - } - - r.syncWorkersLock.Lock() - r.syncWorkers[key] = syncWorker{ - controller: syncController, - cancel: ctrlCancel, - } - r.syncWorkersLock.Unlock() - - go func() { - log.Infow("Starting sync controller…", "key", key) - if err := syncController.Start(ctrlCtx); err != nil && !errors.Is(err, context.Canceled) { - ctrlCancel() - prlog.Errorw("failed to start sync controller", zap.Error(err)) - } - - prlog.Debug("Stopped sync controller") - - r.syncWorkersLock.Lock() - delete(r.syncWorkers, key) - r.syncWorkersLock.Unlock() - }() - - r.clustersLock.RLock() - defer r.clustersLock.RUnlock() - for name, ec := range r.clusters { - if err := syncController.Engage(ec.ctx, name, ec.cl); err != nil { - prlog.Errorw("failed to engage cluster", zap.Error(err), "cluster", name) - } - } - } - - return nil -} diff --git a/internal/controller/syncmanager/doc.go b/internal/controller/syncmanager/doc.go index d0f2a91..62954f7 100644 --- a/internal/controller/syncmanager/doc.go +++ b/internal/controller/syncmanager/doc.go @@ -15,9 +15,7 @@ limitations under the License. */ /* -Package syncmanager contains a controller that watches the APIExport we manage -in kcp. Once the virtual workspace URL for said APIExport is ready, the -controller will begin to synchronize resources back and forth between kcp -(i.e. all relevant workspaces) and the service cluster. +Package syncmanager contains a controller that, using a DynamicMultiClusterManager, +starts and stop sync controllers for each of the PublishedResources. */ package syncmanager diff --git a/internal/discovery/resource_prober.go b/internal/discovery/resource_prober.go new file mode 100644 index 0000000..e11567a --- /dev/null +++ b/internal/discovery/resource_prober.go @@ -0,0 +1,108 @@ +/* +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 discovery + +import ( + "context" + "fmt" + + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ResourceProber struct { + name string + config *rest.Config + client ctrlruntimeclient.Client +} + +func NewResourceProber(endpointSliceWorkspaceConfig *rest.Config, endpointSliceWorkspaceClient ctrlruntimeclient.Client, endpointSliceName string) *ResourceProber { + return &ResourceProber{ + name: endpointSliceName, + config: endpointSliceWorkspaceConfig, + client: endpointSliceWorkspaceClient, + } +} + +func (p *ResourceProber) HasGVR(ctx context.Context, gvr schema.GroupVersionResource) (bool, error) { + return p.hasAPIThing(ctx, func(apiGroup, version, resource, _ string) bool { + return apiGroup == gvr.Group && version == gvr.Version && resource == gvr.Resource + }) +} + +func (p *ResourceProber) HasGVK(ctx context.Context, gvk schema.GroupVersionKind) (bool, error) { + return p.hasAPIThing(ctx, func(apiGroup, version, _, kind string) bool { + return apiGroup == gvk.Group && version == gvk.Version && kind == gvk.Kind + }) +} + +func (p *ResourceProber) hasAPIThing(ctx context.Context, match matchFunc) (bool, error) { + endpointSlice := &kcpapisv1alpha1.APIExportEndpointSlice{} + if err := p.client.Get(ctx, types.NamespacedName{Name: p.name}, endpointSlice); err != nil { + return false, fmt.Errorf("failed to get APIExportEndpointSlice: %w", err) + } + + // connect to each of the endpoints and check if the resource is available there + for _, endpoint := range endpointSlice.Status.APIExportEndpoints { + has, err := p.hasAPIThingInEndpoint(endpoint.URL, match) + if err != nil || !has { + return has, err + } + } + + return true, nil +} + +type matchFunc func(apiGroup, version, resource, kind string) bool + +func (p *ResourceProber) hasAPIThingInEndpoint(endpoint string, match matchFunc) (bool, error) { + endpointConfig := rest.CopyConfig(p.config) + endpointConfig.Host = endpoint + "/clusters/*" + + discoveryClient, err := discovery.NewDiscoveryClientForConfig(endpointConfig) + if err != nil { + return false, fmt.Errorf("failed to create discovery client: %w", err) + } + + _, resourceLists, err := discoveryClient.ServerGroupsAndResources() + if err != nil { + return false, fmt.Errorf("failed to discover APIs: %w", err) + } + + for _, resList := range resourceLists { + // .Group on an APIResource is empty for built-in resources, so we must + // parse and check GroupVersion of the entire list. + gv, err := schema.ParseGroupVersion(resList.GroupVersion) + if err != nil { + return false, fmt.Errorf("invalid API group version %q reported by Kubernetes: %w", resList.GroupVersion, err) + } + + for _, res := range resList.APIResources { + // res.Version is empty for built-in resources + if match(gv.Group, gv.Version, res.Name, res.Kind) { + return true, nil + } + } + } + + return false, nil +} diff --git a/internal/kcp/apiresourceschema.go b/internal/kcp/apiresourceschema.go index 465f080..8f5c2e9 100644 --- a/internal/kcp/apiresourceschema.go +++ b/internal/kcp/apiresourceschema.go @@ -22,22 +22,22 @@ import ( "github.com/kcp-dev/api-syncagent/internal/crypto" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" - kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func CreateAPIResourceSchema(crd *apiextensionsv1.CustomResourceDefinition, name string, agentName string) (*kcpdevv1alpha1.APIResourceSchema, error) { +func CreateAPIResourceSchema(crd *apiextensionsv1.CustomResourceDefinition, name string, agentName string) (*kcpapisv1alpha1.APIResourceSchema, error) { // prefix is irrelevant as the name is overridden later - converted, err := kcpdevv1alpha1.CRDToAPIResourceSchema(crd, "irrelevant") + converted, err := kcpapisv1alpha1.CRDToAPIResourceSchema(crd, "irrelevant") if err != nil { return nil, fmt.Errorf("failed to convert CRD: %w", err) } - ars := &kcpdevv1alpha1.APIResourceSchema{} + ars := &kcpapisv1alpha1.APIResourceSchema{} ars.TypeMeta = metav1.TypeMeta{ - APIVersion: kcpdevv1alpha1.SchemeGroupVersion.String(), + APIVersion: kcpapisv1alpha1.SchemeGroupVersion.String(), Kind: "APIResourceSchema", } @@ -55,9 +55,9 @@ func CreateAPIResourceSchema(crd *apiextensionsv1.CustomResourceDefinition, name ars.Spec.Versions = converted.Spec.Versions if len(converted.Spec.Versions) > 1 { - ars.Spec.Conversion = &kcpdevv1alpha1.CustomResourceConversion{ + ars.Spec.Conversion = &kcpapisv1alpha1.CustomResourceConversion{ // as of kcp 0.27, there is no constant for this - Strategy: kcpdevv1alpha1.ConversionStrategyType("None"), + Strategy: kcpapisv1alpha1.ConversionStrategyType("None"), } } diff --git a/internal/kcp/multicluster.go b/internal/kcp/multicluster.go new file mode 100644 index 0000000..6214e06 --- /dev/null +++ b/internal/kcp/multicluster.go @@ -0,0 +1,222 @@ +/* +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 kcp + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/google/uuid" + "go.uber.org/zap" + + apiexportprovider "github.com/kcp-dev/multicluster-provider/apiexport" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" +) + +// DynamicMultiClusterManager (DMCM) is an extension to the regular multicluster manager. +// It's capable of starting new controllers at any time by keeping track of all +// engaged clusters (so that controllers that start later can be pre-seeded). +// Likewise it's possible to stop any of the controllers at any time. +// +// The DMCM interface is goroutine-safe. +type DynamicMultiClusterManager struct { + manager mcmanager.Manager + + runnablesLock sync.RWMutex + // A map of sync controllers, one for each PublishedResource, using their + // UIDs and resourceVersion as the map keys; using the version ensures that + // when a PR changes, the old controller is orphaned and will be shut down. + runnables map[string]mcmanager.Runnable + + tracker *clusterTracker +} + +func NewDynamicMultiClusterManager(cfg *rest.Config, endpointSliceName string) (*DynamicMultiClusterManager, error) { + scheme := runtime.NewScheme() + + if err := corev1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", corev1.SchemeGroupVersion, err) + } + + if err := kcpapisv1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", kcpapisv1alpha1.SchemeGroupVersion, err) + } + + if err := kcpcorev1alpha1.AddToScheme(scheme); err != nil { + return nil, fmt.Errorf("failed to register scheme %s: %w", kcpcorev1alpha1.SchemeGroupVersion, err) + } + + // Setup the multicluster provider, it will watch the EndpointSlice on its own. + provider, err := apiexportprovider.New(cfg, endpointSliceName, apiexportprovider.Options{ + Scheme: scheme, + }) + if err != nil { + return nil, fmt.Errorf("failed to create multicluster provider: %w", err) + } + + // 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. + multiClusterManager, err := mcmanager.New(cfg, provider, manager.Options{ + Scheme: scheme, + LeaderElection: false, + Metrics: server.Options{ + BindAddress: "0", + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to initialize manager: %w", err) + } + + dynManager := &DynamicMultiClusterManager{ + manager: multiClusterManager, + runnablesLock: sync.RWMutex{}, + runnables: map[string]mcmanager.Runnable{}, + } + + tracker := newClusterTracker(dynManager) + dynManager.tracker = tracker + + // Start our tracker as the first and most important runnable in this mcmanager. + if err := multiClusterManager.Add(tracker); err != nil { + return nil, fmt.Errorf("failed to initialize cluster: %w", err) + } + + return dynManager, nil +} + +func (dmcm *DynamicMultiClusterManager) GetManager() mcmanager.Manager { + return dmcm.manager +} + +func (dmcm *DynamicMultiClusterManager) Start(ctx context.Context) error { + return dmcm.manager.Start(ctx) +} + +// StartController is like pre-seeding the new controller, adding it to the DMCM and +// then starting it in its own goroutine. +func (dmcm *DynamicMultiClusterManager) StartController(ctx context.Context, log *zap.SugaredLogger, controller mcmanager.Runnable) error { + // we need some sort of map key, but it does not matter what that key is + mapKey := uuid.New().String() + + // keep track of this controller so we can forward the Engage() calls to it + dmcm.runnablesLock.Lock() + defer dmcm.runnablesLock.Unlock() + dmcm.runnables[mapKey] = controller + + // start it + log = log.With("component", "DynamicMultiClusterManager", "dmcmkey", mapKey) + + go func() { + log.Debug("Starting controller") + if err := controller.Start(ctx); err != nil && !errors.Is(err, context.Canceled) { + log.Errorw("Controller failed to start", zap.Error(err)) + } + + log.Debug("Removing controller") + dmcm.runnablesLock.Lock() + delete(dmcm.runnables, mapKey) + dmcm.runnablesLock.Unlock() + }() + + // pre-seed the controller + if err := dmcm.tracker.PreSeedController(controller); err != nil { + return fmt.Errorf("failed to pre-seed controller: %w", err) + } + + return nil +} + +type engagedCluster struct { + ctx context.Context + cl cluster.Cluster +} + +// clusterTracker is a dummy controller, which doesn't do anything besides keeping +// track of all Engage() calls, so the DMCM can pre-seed new controllers. +type clusterTracker struct { + dynManager *DynamicMultiClusterManager + lock sync.RWMutex + clusters map[string]engagedCluster +} + +func newClusterTracker(dmcm *DynamicMultiClusterManager) *clusterTracker { + return &clusterTracker{ + dynManager: dmcm, + lock: sync.RWMutex{}, + clusters: map[string]engagedCluster{}, + } +} + +func (s *clusterTracker) Start(_ context.Context) error { + // NOP, this shim doesn't need any further setup + return nil +} + +func (s *clusterTracker) Engage(ctx context.Context, clusterName string, cl cluster.Cluster) error { + if _, ok := s.clusters[clusterName]; !ok { + s.lock.Lock() + s.clusters[clusterName] = engagedCluster{ctx: ctx, cl: cl} + s.lock.Unlock() + + // start a goroutine to make sure we remove the cluster when the context is done + go func() { + <-ctx.Done() + + s.lock.Lock() + defer s.lock.Unlock() + + delete(s.clusters, clusterName) + }() + } + + // forward the Engage() call to all running controllers + s.dynManager.runnablesLock.RLock() + defer s.dynManager.runnablesLock.RUnlock() + + for _, ctrl := range s.dynManager.runnables { + if err := ctrl.Engage(ctx, clusterName, cl); err != nil { + return err + } + } + + return nil +} + +func (s *clusterTracker) PreSeedController(controller mcmanager.Runnable) error { + s.lock.RLock() + defer s.lock.RUnlock() + + for name, engaged := range s.clusters { + if err := controller.Engage(engaged.ctx, name, engaged.cl); err != nil { + return fmt.Errorf("failed to engage with cluster: %w", err) + } + } + + return nil +} diff --git a/internal/mutation/transformer/cel.go b/internal/mutation/transformer/cel.go index b167aa6..1c3ee78 100644 --- a/internal/mutation/transformer/cel.go +++ b/internal/mutation/transformer/cel.go @@ -20,7 +20,7 @@ import ( "fmt" "github.com/google/cel-go/cel" - "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/decls" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -35,10 +35,10 @@ type celTransformer struct { } func NewCEL(mut *syncagentv1alpha1.ResourceCELMutation) (*celTransformer, error) { - env, err := cel.NewEnv(cel.Declarations( - decls.NewVar("self", decls.Dyn), - decls.NewVar("other", decls.Dyn), - decls.NewVar("value", decls.Dyn), + env, err := cel.NewEnv(cel.VariableDecls( + decls.NewVariable("self", cel.DynType), + decls.NewVariable("other", cel.DynType), + decls.NewVariable("value", cel.DynType), )) if err != nil { return nil, fmt.Errorf("failed to create CEL env: %w", err) diff --git a/internal/projection/projection.go b/internal/projection/projection.go index 16b62bd..3fba3a4 100644 --- a/internal/projection/projection.go +++ b/internal/projection/projection.go @@ -134,6 +134,19 @@ func ProjectPublishedResource(ctx context.Context, client *discovery.Client, pub return projectedCRD, nil } +func ProjectPublishedResourceGVK(ctx context.Context, client *discovery.Client, pubRes *syncagentv1alpha1.PublishedResource) (schema.GroupVersionKind, error) { + // find the resource that the PublishedResource is referring to + localGK := PublishedResourceSourceGK(pubRes) + + // fetch the original, full CRD from the cluster + crd, err := client.RetrieveCRD(ctx, localGK) + if err != nil { + return schema.GroupVersionKind{}, fmt.Errorf("failed to discover resource defined in PublishedResource: %w", err) + } + + return PublishedResourceProjectedGVK(crd, pubRes) +} + func RelatedResourceGVR(rr *syncagentv1alpha1.RelatedResourceSpec) schema.GroupVersionResource { resultGVR := schema.GroupVersionResource{ Group: rr.Group, diff --git a/internal/resources/reconciling/zz_generated_reconcile.go b/internal/resources/reconciling/zz_generated_reconcile.go index 0a22759..f19e52c 100644 --- a/internal/resources/reconciling/zz_generated_reconcile.go +++ b/internal/resources/reconciling/zz_generated_reconcile.go @@ -24,7 +24,7 @@ import ( "k8s.io/apimachinery/pkg/types" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" - kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpdevv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" ) // APIExportReconciler defines an interface to create/update APIExports. diff --git a/internal/sync/meta.go b/internal/sync/meta.go index 69ca51d..c5bf76f 100644 --- a/internal/sync/meta.go +++ b/internal/sync/meta.go @@ -19,11 +19,12 @@ package sync import ( "context" - "github.com/kcp-dev/logicalcluster/v3" "go.uber.org/zap" "github.com/kcp-dev/api-syncagent/internal/crypto" + "github.com/kcp-dev/logicalcluster/v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" diff --git a/internal/sync/metadata.go b/internal/sync/metadata.go index afcb848..d42d5c0 100644 --- a/internal/sync/metadata.go +++ b/internal/sync/metadata.go @@ -21,8 +21,6 @@ import ( "maps" "strings" - mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -30,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" ) func stripMetadata(obj *unstructured.Unstructured) error { diff --git a/internal/sync/object_syncer.go b/internal/sync/object_syncer.go index eb4db73..a514e96 100644 --- a/internal/sync/object_syncer.go +++ b/internal/sync/object_syncer.go @@ -22,12 +22,13 @@ import ( "slices" jsonpatch "github.com/evanphx/json-patch/v5" - "github.com/kcp-dev/logicalcluster/v3" "go.uber.org/zap" "k8c.io/reconciler/pkg/equality" "github.com/kcp-dev/api-syncagent/internal/mutation" + "github.com/kcp-dev/logicalcluster/v3" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" diff --git a/internal/sync/state_store.go b/internal/sync/state_store.go index 47fab7e..6cd8641 100644 --- a/internal/sync/state_store.go +++ b/internal/sync/state_store.go @@ -21,10 +21,10 @@ import ( "fmt" "strings" - "github.com/kcp-dev/logicalcluster/v3" - "github.com/kcp-dev/api-syncagent/internal/crypto" + "github.com/kcp-dev/logicalcluster/v3" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" diff --git a/internal/sync/syncer.go b/internal/sync/syncer.go index a741ed5..7aad7f2 100644 --- a/internal/sync/syncer.go +++ b/internal/sync/syncer.go @@ -20,7 +20,6 @@ import ( "context" "fmt" - "github.com/kcp-dev/logicalcluster/v3" "go.uber.org/zap" "github.com/kcp-dev/api-syncagent/internal/mutation" @@ -28,6 +27,8 @@ import ( "github.com/kcp-dev/api-syncagent/internal/sync/templating" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" diff --git a/internal/sync/syncer_test.go b/internal/sync/syncer_test.go index 0b21532..75055e8 100644 --- a/internal/sync/syncer_test.go +++ b/internal/sync/syncer_test.go @@ -23,7 +23,6 @@ import ( "os" "testing" - "github.com/kcp-dev/logicalcluster/v3" "go.uber.org/zap" "github.com/kcp-dev/api-syncagent/internal/mutation" @@ -31,6 +30,8 @@ import ( "github.com/kcp-dev/api-syncagent/internal/test/diff" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/internal/sync/templating/naming.go b/internal/sync/templating/naming.go index e3f76a9..32058b3 100644 --- a/internal/sync/templating/naming.go +++ b/internal/sync/templating/naming.go @@ -20,11 +20,11 @@ import ( "fmt" "strings" - "github.com/kcp-dev/logicalcluster/v3" - "github.com/kcp-dev/api-syncagent/internal/crypto" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" ) diff --git a/internal/sync/templating/naming_test.go b/internal/sync/templating/naming_test.go index a72541c..471ae63 100644 --- a/internal/sync/templating/naming_test.go +++ b/internal/sync/templating/naming_test.go @@ -19,10 +19,10 @@ package templating import ( "testing" - "github.com/kcp-dev/logicalcluster/v3" - syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" ) diff --git a/internal/sync/templating/related.go b/internal/sync/templating/related.go index 1097448..dcaf0ce 100644 --- a/internal/sync/templating/related.go +++ b/internal/sync/templating/related.go @@ -17,10 +17,10 @@ limitations under the License. package templating import ( - "github.com/kcp-dev/logicalcluster/v3" - syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) diff --git a/internal/sync/templating/templating_test.go b/internal/sync/templating/templating_test.go index e432d79..9af6e2b 100644 --- a/internal/sync/templating/templating_test.go +++ b/internal/sync/templating/templating_test.go @@ -19,11 +19,11 @@ package templating import ( "testing" - "github.com/kcp-dev/logicalcluster/v3" - crds "github.com/kcp-dev/api-syncagent/test/crds/example/v1" "github.com/kcp-dev/api-syncagent/test/utils" + "github.com/kcp-dev/logicalcluster/v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/test/e2e/apiexport/apiexport_test.go b/test/e2e/apiexport/apiexport_test.go index 06371a7..f637ff3 100644 --- a/test/e2e/apiexport/apiexport_test.go +++ b/test/e2e/apiexport/apiexport_test.go @@ -30,7 +30,7 @@ import ( syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" "github.com/kcp-dev/api-syncagent/test/utils" - kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/test/e2e/apiresourceschema/apiresourceschema_test.go b/test/e2e/apiresourceschema/apiresourceschema_test.go index 136c88d..edfbb5c 100644 --- a/test/e2e/apiresourceschema/apiresourceschema_test.go +++ b/test/e2e/apiresourceschema/apiresourceschema_test.go @@ -29,7 +29,7 @@ import ( syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" "github.com/kcp-dev/api-syncagent/test/utils" - kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/test/e2e/sync/apiexportendpointslice_test.go b/test/e2e/sync/apiexportendpointslice_test.go index b8930dd..f8073f2 100644 --- a/test/e2e/sync/apiexportendpointslice_test.go +++ b/test/e2e/sync/apiexportendpointslice_test.go @@ -25,12 +25,12 @@ import ( "time" "github.com/go-logr/logr" - "github.com/kcp-dev/logicalcluster/v3" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" "github.com/kcp-dev/api-syncagent/test/utils" - kcpdevv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + kcpdevv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -88,8 +88,8 @@ func TestAPIExportEndpointSliceSameCluster(t *testing.T) { t.Fatalf("Failed to create PublishedResource: %v", err) } - // In kcp 0.27, we have to manually create the AEES. To make this test work consistently with - // 0.27 and later versions, we simply always create one. + // Every APIExport we create with CreateOrganization already has an EndpointSlice, + // but we create a custom one just to prove a point. kcpClusterClient := utils.GetKcpAdminClusterClient(t) orgClient := kcpClusterClient.Cluster(logicalcluster.NewPath("root").Join(orgWorkspace)) @@ -110,7 +110,7 @@ func TestAPIExportEndpointSliceSameCluster(t *testing.T) { } // start the agent in the background to update the APIExport with the CronTabs API; - utils.RunEndpointSliceAgent(ctx, t, "bob", orgKubconfig, envtestKubeconfig, endpointSlice.Name, "") + utils.RunAgent(ctx, t, "bob", orgKubconfig, envtestKubeconfig, endpointSlice.Name, "") // wait until the API is available teamClusterPath := logicalcluster.NewPath("root").Join(orgWorkspace).Join("team-1") @@ -253,7 +253,7 @@ func TestAPIExportEndpointSliceDifferentCluster(t *testing.T) { } // start the agent in the background to update the APIExport with the CronTabs API - utils.RunEndpointSliceAgent(ctx, t, "bob", endpointKubeconfig, envtestKubeconfig, endpointSlice.Name, "") + utils.RunAgent(ctx, t, "bob", endpointKubeconfig, envtestKubeconfig, endpointSlice.Name, "") // wait until the API is available diff --git a/test/e2e/sync/primary_test.go b/test/e2e/sync/primary_test.go index 52e501a..6ed78cc 100644 --- a/test/e2e/sync/primary_test.go +++ b/test/e2e/sync/primary_test.go @@ -27,11 +27,12 @@ import ( "time" "github.com/go-logr/logr" - "github.com/kcp-dev/logicalcluster/v3" syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" "github.com/kcp-dev/api-syncagent/test/utils" + "github.com/kcp-dev/logicalcluster/v3" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" diff --git a/test/e2e/sync/related_test.go b/test/e2e/sync/related_test.go index 03843a0..ea724e3 100644 --- a/test/e2e/sync/related_test.go +++ b/test/e2e/sync/related_test.go @@ -28,7 +28,6 @@ import ( "time" "github.com/go-logr/logr" - "github.com/kcp-dev/logicalcluster/v3" "github.com/kcp-dev/api-syncagent/internal/projection" "github.com/kcp-dev/api-syncagent/internal/test/diff" @@ -36,7 +35,8 @@ import ( crds "github.com/kcp-dev/api-syncagent/test/crds/example/v1" "github.com/kcp-dev/api-syncagent/test/utils" - kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + "github.com/kcp-dev/logicalcluster/v3" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" diff --git a/test/utils/fixtures.go b/test/utils/fixtures.go index e9abfe7..0005412 100644 --- a/test/utils/fixtures.go +++ b/test/utils/fixtures.go @@ -26,12 +26,11 @@ import ( "time" "github.com/kcp-dev/logicalcluster/v3" - - kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" - kcpcorev1alpha1 "github.com/kcp-dev/kcp/sdk/apis/core/v1alpha1" - kcptenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" - conditionsv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" - "github.com/kcp-dev/kcp/sdk/apis/third_party/conditions/util/conditions" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" + kcpcorev1alpha1 "github.com/kcp-dev/sdk/apis/core/v1alpha1" + kcptenancyv1alpha1 "github.com/kcp-dev/sdk/apis/tenancy/v1alpha1" + conditionsv1alpha1 "github.com/kcp-dev/sdk/apis/third_party/conditions/apis/conditions/v1alpha1" + "github.com/kcp-dev/sdk/apis/third_party/conditions/util/conditions" rbacv1 "k8s.io/api/rbac/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -129,6 +128,24 @@ func CreateAPIExport(t *testing.T, ctx context.Context, client ctrlruntimeclient t.Fatalf("Failed to create APIExport: %v", err) } + // In kcp 0.27, we have to manually create the AEES. To make the tests work consistently with + // 0.27 and later versions, we simply always create one. + endpointSlice := &kcpapisv1alpha1.APIExportEndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: kcpapisv1alpha1.APIExportEndpointSliceSpec{ + APIExport: kcpapisv1alpha1.ExportBindingReference{ + Name: name, + }, + }, + } + + t.Logf("Creating APIExportEndpointSlice %q…", endpointSlice.Name) + if err := client.Create(ctx, endpointSlice); err != nil { + t.Fatalf("Failed to create APIExportEndpointSlice: %v", err) + } + // grant permissions to access/manage the APIExport if rbacSubject != nil { clusterRoleName := "api-syncagent" diff --git a/test/utils/process.go b/test/utils/process.go index ca3690a..e9cbbab 100644 --- a/test/utils/process.go +++ b/test/utils/process.go @@ -68,38 +68,13 @@ func uniqueLogfile(t *testing.T, basename string) string { return fmt.Sprintf("%s_%02d.log", testName, counter) } -func RunEndpointSliceAgent( - ctx context.Context, - t *testing.T, - name string, - kcpKubeconfig string, - localKubeconfig string, - apiExportEndpointSlice string, - labelSelector string, -) context.CancelFunc { - return runAgent(ctx, t, name, kcpKubeconfig, localKubeconfig, "--apiexportendpointslice-ref", apiExportEndpointSlice, labelSelector) -} - func RunAgent( ctx context.Context, t *testing.T, name string, kcpKubeconfig string, localKubeconfig string, - apiExport string, - labelSelector string, -) context.CancelFunc { - return runAgent(ctx, t, name, kcpKubeconfig, localKubeconfig, "--apiexport-ref", apiExport, labelSelector) -} - -func runAgent( - ctx context.Context, - t *testing.T, - name string, - kcpKubeconfig string, - localKubeconfig string, - refFlag string, - refValue string, + apiExportEndpointSlice string, labelSelector string, ) context.CancelFunc { t.Helper() @@ -108,7 +83,7 @@ func runAgent( args := []string{ "--agent-name", name, - refFlag, refValue, + "--apiexportendpointslice-ref", apiExportEndpointSlice, "--enable-leader-election=false", "--kubeconfig", localKubeconfig, "--kcp-kubeconfig", kcpKubeconfig, diff --git a/test/utils/utils.go b/test/utils/utils.go index 5102d92..a79d38e 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -27,9 +27,9 @@ import ( syncagentv1alpha1 "github.com/kcp-dev/api-syncagent/sdk/apis/syncagent/v1alpha1" - kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" - kcptenancyv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/tenancy/v1alpha1" mcclient "github.com/kcp-dev/multicluster-provider/client" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" + kcptenancyv1alpha1 "github.com/kcp-dev/sdk/apis/tenancy/v1alpha1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" diff --git a/test/utils/wait.go b/test/utils/wait.go index b8fe446..c2ea631 100644 --- a/test/utils/wait.go +++ b/test/utils/wait.go @@ -22,7 +22,7 @@ import ( "testing" "time" - kcpapisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1" + kcpapisv1alpha1 "github.com/kcp-dev/sdk/apis/apis/v1alpha1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types"