diff --git a/cmd/milo/controller-manager/controllermanager.go b/cmd/milo/controller-manager/controllermanager.go index 87bf5e30..20793e0f 100644 --- a/cmd/milo/controller-manager/controllermanager.go +++ b/cmd/milo/controller-manager/controllermanager.go @@ -98,6 +98,7 @@ import ( quotav1alpha1 "go.miloapis.com/milo/pkg/apis/quota/v1alpha1" resourcemanagerv1alpha1 "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" miloprovider "go.miloapis.com/milo/pkg/multicluster-runtime/milo" + milowebhook "go.miloapis.com/milo/pkg/webhook" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" ) @@ -471,7 +472,11 @@ func Run(ctx context.Context, c *config.CompletedConfig, opts *Options) error { MapperProvider: func(c *restclient.Config, httpClient *http.Client) (meta.RESTMapper, error) { return controllerContext.RESTMapper, nil }, - WebhookServer: webhook.NewServer(webhook.Options{ + // Wrap the webhook server with ClusterAwareServer to automatically inject + // project control plane context from UserInfo.Extra into the request context. + // This allows webhook handlers to use mccontext.ClusterFrom(ctx) to determine + // which project control plane the request is targeting. + WebhookServer: milowebhook.NewClusterAwareServer(webhook.NewServer(webhook.Options{ Port: opts.ControllerRuntimeWebhookPort, CertDir: opts.SecureServing.ServerCert.CertDirectory, // The webhook server expects the key and cert files to be in the @@ -481,7 +486,7 @@ func Run(ctx context.Context, c *config.CompletedConfig, opts *Options) error { // key and cert file names. KeyName: strings.TrimPrefix(opts.SecureServing.ServerCert.CertKey.KeyFile, opts.SecureServing.ServerCert.CertDirectory+"/"), CertName: strings.TrimPrefix(opts.SecureServing.ServerCert.CertKey.CertFile, opts.SecureServing.ServerCert.CertDirectory+"/"), - }), + })), }, ) if err != nil { @@ -553,14 +558,8 @@ func Run(ctx context.Context, c *config.CompletedConfig, opts *Options) error { logger.Error(err, "Error setting up platform access rejection webhook") klog.FlushAndExit(klog.ExitFlushTimeout, 1) } - if err := notesv1alpha1webhook.SetupNoteWebhooksWithManager(ctrl); err != nil { - logger.Error(err, "Error setting up note webhook") - klog.FlushAndExit(klog.ExitFlushTimeout, 1) - } - if err := notesv1alpha1webhook.SetupClusterNoteWebhooksWithManager(ctrl); err != nil { - logger.Error(err, "Error setting up clusternote webhook") - klog.FlushAndExit(klog.ExitFlushTimeout, 1) - } + // Note webhooks are set up later after the multicluster manager is created, + // so they can use it for project control plane lookups. projectCtrl := resourcemanagercontroller.ProjectController{ ControlPlaneClient: ctrl.GetClient(), @@ -666,6 +665,16 @@ func Run(ctx context.Context, c *config.CompletedConfig, opts *Options) error { } logger.Info("Local cluster engaged successfully") + // Set up note webhooks with multicluster manager for project control plane lookups + if err := notesv1alpha1webhook.SetupNoteWebhooksWithManager(ctrl, mcMgr); err != nil { + logger.Error(err, "Error setting up note webhook") + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } + if err := notesv1alpha1webhook.SetupClusterNoteWebhooksWithManager(ctrl, mcMgr); err != nil { + logger.Error(err, "Error setting up clusternote webhook") + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } + // Start concurrently to resolve circular dependency between provider and manager go func() { logger.Info("Starting Datum cluster provider") diff --git a/internal/webhooks/notes/v1alpha1/clusternote_webhook.go b/internal/webhooks/notes/v1alpha1/clusternote_webhook.go index 5559a8d5..689b3d64 100644 --- a/internal/webhooks/notes/v1alpha1/clusternote_webhook.go +++ b/internal/webhooks/notes/v1alpha1/clusternote_webhook.go @@ -19,18 +19,21 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" ) var clusterNoteLog = logf.Log.WithName("clusternote-resource") -func SetupClusterNoteWebhooksWithManager(mgr ctrl.Manager) error { +func SetupClusterNoteWebhooksWithManager(mgr ctrl.Manager, mcMgr mcmanager.Manager) error { clusterNoteLog.Info("Setting up notes.miloapis.com clusternote webhooks") return ctrl.NewWebhookManagedBy(mgr). For(¬esv1alpha1.ClusterNote{}). WithDefaulter(&ClusterNoteMutator{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - RESTMapper: mgr.GetRESTMapper(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + RESTMapper: mgr.GetRESTMapper(), + ClusterManager: mcMgr, }). WithValidator(&ClusterNoteValidator{ Client: mgr.GetClient(), @@ -41,9 +44,10 @@ func SetupClusterNoteWebhooksWithManager(mgr ctrl.Manager) error { // +kubebuilder:webhook:path=/mutate-notes-miloapis-com-v1alpha1-clusternote,mutating=true,failurePolicy=fail,sideEffects=None,groups=notes.miloapis.com,resources=clusternotes,verbs=create,versions=v1alpha1,name=mclusternote.notes.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system type ClusterNoteMutator struct { - Client client.Client - Scheme *runtime.Scheme - RESTMapper meta.RESTMapper + Client client.Client + Scheme *runtime.Scheme + RESTMapper meta.RESTMapper + ClusterManager mcmanager.Manager } var _ admission.CustomDefaulter = &ClusterNoteMutator{} @@ -70,7 +74,6 @@ func (m *ClusterNoteMutator) Default(ctx context.Context, obj runtime.Object) er } // Set owner reference to the subject resource for automatic garbage collection - // This is critical for the ClusterNote to be garbage collected when the subject is deleted if err := m.setSubjectOwnerReference(ctx, clusterNote); err != nil { clusterNoteLog.Error(err, "Failed to set owner reference to subject", "clusternote", clusterNote.Name) return errors.NewInternalError(fmt.Errorf("failed to set owner reference to subject: %w", err)) @@ -79,7 +82,8 @@ func (m *ClusterNoteMutator) Default(ctx context.Context, obj runtime.Object) er return nil } -// setSubjectOwnerReference sets the owner reference to the subject resource if it's cluster-scoped +// setSubjectOwnerReference sets the owner reference to the subject resource if it's cluster-scoped. +// The cluster context is expected to be injected by the ClusterAwareServer wrapper. func (m *ClusterNoteMutator) setSubjectOwnerReference(ctx context.Context, clusterNote *notesv1alpha1.ClusterNote) error { // ClusterNote can only have owner references to other cluster-scoped resources if clusterNote.Spec.SubjectRef.Namespace != "" { @@ -97,24 +101,34 @@ func (m *ClusterNoteMutator) setSubjectOwnerReference(ctx context.Context, clust return fmt.Errorf("failed to get REST mapping for %s: %w", groupKind, err) } + key := types.NamespacedName{ + Name: clusterNote.Spec.SubjectRef.Name, + } + + // Determine which client to use based on cluster context (injected by ClusterAwareServer) + subjectClient := m.Client + + if m.ClusterManager != nil { + if clusterName, ok := mccontext.ClusterFrom(ctx); ok && clusterName != "" { + cluster, err := m.ClusterManager.GetCluster(ctx, clusterName) + if err != nil { + return fmt.Errorf("failed to get project control plane %s: %w", clusterName, err) + } + subjectClient = cluster.GetClient() + clusterNoteLog.V(1).Info("Using project control plane client", "cluster", clusterName) + } + } + subject := &unstructured.Unstructured{} subject.SetGroupVersionKind(mapping.GroupVersionKind) - - if err := m.Client.Get(ctx, types.NamespacedName{ - Name: clusterNote.Spec.SubjectRef.Name, - }, subject); err != nil { + if err := subjectClient.Get(ctx, key, subject); err != nil { if errors.IsNotFound(err) { return fmt.Errorf("subject resource not found: %w", err) } return fmt.Errorf("failed to get subject resource: %w", err) } - // Set owner reference - if err := controllerutil.SetOwnerReference(subject, clusterNote, m.Scheme); err != nil { - return fmt.Errorf("failed to set owner reference: %w", err) - } - - return nil + return controllerutil.SetOwnerReference(subject, clusterNote, m.Scheme) } // +kubebuilder:webhook:path=/validate-notes-miloapis-com-v1alpha1-clusternote,mutating=false,failurePolicy=fail,sideEffects=None,groups=notes.miloapis.com,resources=clusternotes,verbs=create;update,versions=v1alpha1,name=vclusternote.notes.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system diff --git a/internal/webhooks/notes/v1alpha1/clusternote_webhook_test.go b/internal/webhooks/notes/v1alpha1/clusternote_webhook_test.go index e3a4b0af..a69777f5 100644 --- a/internal/webhooks/notes/v1alpha1/clusternote_webhook_test.go +++ b/internal/webhooks/notes/v1alpha1/clusternote_webhook_test.go @@ -126,9 +126,10 @@ func TestClusterNoteMutator_Default(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(objects...).Build() mutator := &ClusterNoteMutator{ - Client: fakeClient, - Scheme: testScheme, - RESTMapper: newTestRESTMapper(), + Client: fakeClient, + Scheme: testScheme, + RESTMapper: newTestRESTMapper(), + ClusterManager: nil, // nil means local-only search (backwards compatible) } // Create admission request context diff --git a/internal/webhooks/notes/v1alpha1/note_webhook.go b/internal/webhooks/notes/v1alpha1/note_webhook.go index 298294fd..c0ce545f 100644 --- a/internal/webhooks/notes/v1alpha1/note_webhook.go +++ b/internal/webhooks/notes/v1alpha1/note_webhook.go @@ -19,18 +19,21 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" ) var noteLog = logf.Log.WithName("note-resource") -func SetupNoteWebhooksWithManager(mgr ctrl.Manager) error { +func SetupNoteWebhooksWithManager(mgr ctrl.Manager, mcMgr mcmanager.Manager) error { noteLog.Info("Setting up notes.miloapis.com note webhooks") return ctrl.NewWebhookManagedBy(mgr). For(¬esv1alpha1.Note{}). WithDefaulter(&NoteMutator{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - RESTMapper: mgr.GetRESTMapper(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + RESTMapper: mgr.GetRESTMapper(), + ClusterManager: mcMgr, }). WithValidator(&NoteValidator{ Client: mgr.GetClient(), @@ -41,9 +44,10 @@ func SetupNoteWebhooksWithManager(mgr ctrl.Manager) error { // +kubebuilder:webhook:path=/mutate-notes-miloapis-com-v1alpha1-note,mutating=true,failurePolicy=fail,sideEffects=None,groups=notes.miloapis.com,resources=notes,verbs=create,versions=v1alpha1,name=mnote.notes.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system type NoteMutator struct { - Client client.Client - Scheme *runtime.Scheme - RESTMapper meta.RESTMapper + Client client.Client + Scheme *runtime.Scheme + RESTMapper meta.RESTMapper + ClusterManager mcmanager.Manager } var _ admission.CustomDefaulter = &NoteMutator{} @@ -70,7 +74,6 @@ func (m *NoteMutator) Default(ctx context.Context, obj runtime.Object) error { } // Set owner reference to the subject resource for automatic garbage collection - // This is critical for the Note to be garbage collected when the subject is deleted if err := m.setSubjectOwnerReference(ctx, note); err != nil { noteLog.Error(err, "Failed to set owner reference to subject", "note", note.Name) return errors.NewInternalError(fmt.Errorf("failed to set owner reference to subject: %w", err)) @@ -79,14 +82,15 @@ func (m *NoteMutator) Default(ctx context.Context, obj runtime.Object) error { return nil } -// setSubjectOwnerReference sets the owner reference to the subject resource if it's in the same namespace +// setSubjectOwnerReference sets the owner reference to the subject resource if it's in the same namespace. +// The cluster context is expected to be injected by the ClusterAwareServer wrapper. func (m *NoteMutator) setSubjectOwnerReference(ctx context.Context, note *notesv1alpha1.Note) error { // Only set owner reference if the subject is in the same namespace if note.Spec.SubjectRef.Namespace == "" || note.Spec.SubjectRef.Namespace != note.Namespace { return nil } - // Resolve the GVK using REST mapper to discover the correct API version + // Resolve the GVK using REST mapper groupKind := schema.GroupKind{ Group: note.Spec.SubjectRef.APIGroup, Kind: note.Spec.SubjectRef.Kind, @@ -97,25 +101,35 @@ func (m *NoteMutator) setSubjectOwnerReference(ctx context.Context, note *notesv return fmt.Errorf("failed to get REST mapping for %s: %w", groupKind, err) } - subject := &unstructured.Unstructured{} - subject.SetGroupVersionKind(mapping.GroupVersionKind) - - if err := m.Client.Get(ctx, types.NamespacedName{ + key := types.NamespacedName{ Name: note.Spec.SubjectRef.Name, Namespace: note.Spec.SubjectRef.Namespace, - }, subject); err != nil { + } + + // Determine which client to use based on cluster context (injected by ClusterAwareServer) + subjectClient := m.Client + + if m.ClusterManager != nil { + if clusterName, ok := mccontext.ClusterFrom(ctx); ok && clusterName != "" { + cluster, err := m.ClusterManager.GetCluster(ctx, clusterName) + if err != nil { + return fmt.Errorf("failed to get project control plane %s: %w", clusterName, err) + } + subjectClient = cluster.GetClient() + noteLog.V(1).Info("Using project control plane client", "cluster", clusterName) + } + } + + subject := &unstructured.Unstructured{} + subject.SetGroupVersionKind(mapping.GroupVersionKind) + if err := subjectClient.Get(ctx, key, subject); err != nil { if errors.IsNotFound(err) { return fmt.Errorf("subject resource not found: %w", err) } return fmt.Errorf("failed to get subject resource: %w", err) } - // Set owner reference - if err := controllerutil.SetOwnerReference(subject, note, m.Scheme); err != nil { - return fmt.Errorf("failed to set owner reference: %w", err) - } - - return nil + return controllerutil.SetOwnerReference(subject, note, m.Scheme) } // +kubebuilder:webhook:path=/validate-notes-miloapis-com-v1alpha1-note,mutating=false,failurePolicy=fail,sideEffects=None,groups=notes.miloapis.com,resources=notes,verbs=create;update,versions=v1alpha1,name=vnote.notes.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system diff --git a/internal/webhooks/notes/v1alpha1/note_webhook_test.go b/internal/webhooks/notes/v1alpha1/note_webhook_test.go index 684b010a..08746ef9 100644 --- a/internal/webhooks/notes/v1alpha1/note_webhook_test.go +++ b/internal/webhooks/notes/v1alpha1/note_webhook_test.go @@ -160,9 +160,10 @@ func TestNoteMutator_Default(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(testScheme).WithObjects(objects...).Build() mutator := &NoteMutator{ - Client: fakeClient, - Scheme: testScheme, - RESTMapper: newTestRESTMapper(), + Client: fakeClient, + Scheme: testScheme, + RESTMapper: newTestRESTMapper(), + ClusterManager: nil, // nil means local-only search (backwards compatible) } // Create admission request context diff --git a/pkg/multicluster-runtime/milo/provider.go b/pkg/multicluster-runtime/milo/provider.go index 2055d331..e71bbd2a 100644 --- a/pkg/multicluster-runtime/milo/provider.go +++ b/pkg/multicluster-runtime/milo/provider.go @@ -156,7 +156,9 @@ func (p *Provider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result log := p.log.WithValues("project", req.Name) log.Info("Reconciling Project") - key := req.String() + // Use just the project name as the key for cluster lookup. + // This matches the project name used in URL paths and ParentNameExtraKey. + key := req.Name var project unstructured.Unstructured if p.opts.InternalServiceDiscovery { diff --git a/pkg/multicluster-runtime/milo/provider_test.go b/pkg/multicluster-runtime/milo/provider_test.go index 449c0158..7f1283f3 100644 --- a/pkg/multicluster-runtime/milo/provider_test.go +++ b/pkg/multicluster-runtime/milo/provider_test.go @@ -61,7 +61,7 @@ func TestReadyProject(t *testing.T) { assert.Zero(t, result.RequeueAfter) assert.Len(t, provider.projects, 1) - cl, err := provider.Get(context.Background(), "/test-project") + cl, err := provider.Get(context.Background(), "test-project") assert.NoError(t, err) apiHost, err := url.Parse(cl.GetConfig().Host) assert.NoError(t, err) diff --git a/pkg/webhook/cluster_aware_server.go b/pkg/webhook/cluster_aware_server.go new file mode 100644 index 00000000..3b7c3c11 --- /dev/null +++ b/pkg/webhook/cluster_aware_server.go @@ -0,0 +1,75 @@ +// Package webhook provides multi-cluster aware webhook utilities for services +// that integrate with Milo's project control plane architecture. +package webhook + +import ( + "context" + "net/http" + + iamv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1" + authv1 "k8s.io/api/authentication/v1" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + mccontext "sigs.k8s.io/multicluster-runtime/pkg/context" +) + +// ClusterAwareServer wraps a webhook.Server to automatically inject the cluster +// name from the request's UserInfo.Extra into the context. This allows webhook +// handlers to use mccontext.ClusterFrom(ctx) to determine which project control +// plane the request is targeting. +// +// The cluster name is extracted from the "iam.miloapis.com/parent-name" extra +// field, which is set by Milo's API server when requests target a project +// control plane via the aggregated API path. +type ClusterAwareServer struct { + webhook.Server +} + +var _ webhook.Server = &ClusterAwareServer{} + +// NewClusterAwareServer wraps a webhook.Server to inject cluster context from +// the request's UserInfo.Extra fields into the handler context. +// +// Example usage: +// +// webhookServer := webhook.NewServer(webhook.Options{...}) +// webhookServer = milowebhook.NewClusterAwareServer(webhookServer) +// mgr.Add(webhookServer) +func NewClusterAwareServer(server webhook.Server) *ClusterAwareServer { + return &ClusterAwareServer{ + Server: server, + } +} + +// Register wraps the webhook handler to inject cluster context before calling +// the original handler. +func (s *ClusterAwareServer) Register(path string, hook http.Handler) { + if h, ok := hook.(*admission.Webhook); ok { + orig := h.Handler + h.Handler = admission.HandlerFunc(func(ctx context.Context, req admission.Request) admission.Response { + clusterName := clusterNameFromExtra(req.UserInfo.Extra) + if clusterName != "" { + ctx = mccontext.WithCluster(ctx, clusterName) + } + return orig.Handle(ctx, req) + }) + } + + s.Server.Register(path, hook) +} + +// clusterNameFromExtra extracts the cluster/project name from the UserInfo.Extra +// fields. Returns empty string if not in a project context. +func clusterNameFromExtra(extra map[string]authv1.ExtraValue) string { + // Check if this is a project context + if parentKinds, ok := extra[iamv1alpha1.ParentKindExtraKey]; !ok || len(parentKinds) == 0 || parentKinds[0] != "Project" { + return "" + } + + // Extract the project name + if parentNames, ok := extra[iamv1alpha1.ParentNameExtraKey]; ok && len(parentNames) > 0 { + return parentNames[0] + } + + return "" +} diff --git a/test/notes/clusternote-multicluster-subject/01-organization.yaml b/test/notes/clusternote-multicluster-subject/01-organization.yaml new file mode 100644 index 00000000..edfa66a7 --- /dev/null +++ b/test/notes/clusternote-multicluster-subject/01-organization.yaml @@ -0,0 +1,8 @@ +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: Organization +metadata: + name: cn-mc-test-org + labels: + test.miloapis.com/notes: "clusternote-multicluster" +spec: + type: Standard diff --git a/test/notes/clusternote-multicluster-subject/chainsaw-test.yaml b/test/notes/clusternote-multicluster-subject/chainsaw-test.yaml new file mode 100644 index 00000000..09053948 --- /dev/null +++ b/test/notes/clusternote-multicluster-subject/chainsaw-test.yaml @@ -0,0 +1,101 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: clusternote-multicluster-subject +spec: + description: | + Tests that ClusterNotes can reference cluster-scoped subjects (Namespaces) + in project control planes. + + Validates: + - ClusterNote in project control plane can reference cluster-scoped Namespace + - Owner reference is correctly set on the ClusterNote + - ClusterNote is garbage collected when Namespace is deleted + + clusters: + main: + kubeconfig: kubeconfig-main + org: + kubeconfig: kubeconfig-org + project: + kubeconfig: kubeconfig-project-1 + + steps: + - name: setup-organization + description: Create test organization + cluster: main + try: + - apply: + file: 01-organization.yaml + - wait: + apiVersion: v1 + kind: Namespace + name: organization-cn-mc-test-org + timeout: 30s + for: + jsonPath: + path: '{.status.phase}' + value: Active + + - name: create-project + description: Create project in org control plane + cluster: org + try: + - apply: + file: test-data/project.yaml + - wait: + apiVersion: resourcemanager.miloapis.com/v1alpha1 + kind: Project + name: cn-mc-test-project-1 + timeout: 60s + for: + condition: + name: Ready + value: 'true' + + - name: create-namespace-in-project + description: Create cluster-scoped Namespace in project control plane + cluster: project + try: + - apply: + file: test-data/namespace.yaml + - wait: + apiVersion: v1 + kind: Namespace + name: cn-test-namespace + timeout: 30s + for: + jsonPath: + path: '{.status.phase}' + value: Active + + - name: create-clusternote-referencing-namespace + description: Create ClusterNote referencing the Namespace in project control plane + cluster: project + try: + - apply: + file: test-data/clusternote.yaml + - assert: + resource: + apiVersion: notes.miloapis.com/v1alpha1 + kind: ClusterNote + metadata: + name: test-namespace-clusternote + (length(metadata.ownerReferences) > `0`): true + + - name: delete-namespace-verify-clusternote-deletion + description: Delete Namespace and verify ClusterNote is garbage collected + cluster: project + try: + - delete: + ref: + apiVersion: v1 + kind: Namespace + name: cn-test-namespace + - wait: + apiVersion: notes.miloapis.com/v1alpha1 + kind: ClusterNote + name: test-namespace-clusternote + timeout: 60s + for: + deletion: {} diff --git a/test/notes/clusternote-multicluster-subject/kubeconfig-main b/test/notes/clusternote-multicluster-subject/kubeconfig-main new file mode 100644 index 00000000..b0496e6c --- /dev/null +++ b/test/notes/clusternote-multicluster-subject/kubeconfig-main @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:30443 + name: milo-test-infra +contexts: +- context: + cluster: milo-test-infra + user: admin + name: milo-test-infra +current-context: milo-test-infra +kind: Config +preferences: {} +users: +- name: admin + user: + token: test-admin-token diff --git a/test/notes/clusternote-multicluster-subject/kubeconfig-org b/test/notes/clusternote-multicluster-subject/kubeconfig-org new file mode 100644 index 00000000..6159602c --- /dev/null +++ b/test/notes/clusternote-multicluster-subject/kubeconfig-org @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:30443/apis/resourcemanager.miloapis.com/v1alpha1/organizations/cn-mc-test-org/control-plane + name: org-cn-mc-test-org +contexts: +- context: + cluster: org-cn-mc-test-org + user: user-1001 + name: org-cn-mc-test-org +current-context: org-cn-mc-test-org +kind: Config +preferences: {} +users: +- name: user-1001 + user: + token: test-admin-token diff --git a/test/notes/clusternote-multicluster-subject/kubeconfig-project-1 b/test/notes/clusternote-multicluster-subject/kubeconfig-project-1 new file mode 100644 index 00000000..9bf46b62 --- /dev/null +++ b/test/notes/clusternote-multicluster-subject/kubeconfig-project-1 @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:30443/apis/resourcemanager.miloapis.com/v1alpha1/projects/cn-mc-test-project-1/control-plane + name: project-cn-mc-test-project-1 +contexts: +- context: + cluster: project-cn-mc-test-project-1 + user: user-1001 + name: project-cn-mc-test-project-1 +current-context: project-cn-mc-test-project-1 +kind: Config +preferences: {} +users: +- name: user-1001 + user: + token: test-admin-token diff --git a/test/notes/clusternote-multicluster-subject/test-data/clusternote.yaml b/test/notes/clusternote-multicluster-subject/test-data/clusternote.yaml new file mode 100644 index 00000000..0494dae7 --- /dev/null +++ b/test/notes/clusternote-multicluster-subject/test-data/clusternote.yaml @@ -0,0 +1,12 @@ +apiVersion: notes.miloapis.com/v1alpha1 +kind: ClusterNote +metadata: + name: test-namespace-clusternote + labels: + test.miloapis.com/notes: "clusternote-multicluster" +spec: + content: "This ClusterNote references a Namespace in the project control plane" + subjectRef: + apiGroup: "" + kind: Namespace + name: cn-test-namespace diff --git a/test/notes/clusternote-multicluster-subject/test-data/namespace.yaml b/test/notes/clusternote-multicluster-subject/test-data/namespace.yaml new file mode 100644 index 00000000..f5a987d1 --- /dev/null +++ b/test/notes/clusternote-multicluster-subject/test-data/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: cn-test-namespace + labels: + test.miloapis.com/notes: "clusternote-multicluster" diff --git a/test/notes/clusternote-multicluster-subject/test-data/project.yaml b/test/notes/clusternote-multicluster-subject/test-data/project.yaml new file mode 100644 index 00000000..4d6eb5a5 --- /dev/null +++ b/test/notes/clusternote-multicluster-subject/test-data/project.yaml @@ -0,0 +1,10 @@ +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: Project +metadata: + name: cn-mc-test-project-1 + labels: + test.miloapis.com/notes: "clusternote-multicluster" +spec: + ownerRef: + kind: Organization + name: cn-mc-test-org diff --git a/test/notes/note-multicluster-subject/01-organization.yaml b/test/notes/note-multicluster-subject/01-organization.yaml new file mode 100644 index 00000000..78ec9cf7 --- /dev/null +++ b/test/notes/note-multicluster-subject/01-organization.yaml @@ -0,0 +1,8 @@ +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: Organization +metadata: + name: note-mc-test-org + labels: + test.miloapis.com/notes: "multicluster" +spec: + type: Standard diff --git a/test/notes/note-multicluster-subject/chainsaw-test.yaml b/test/notes/note-multicluster-subject/chainsaw-test.yaml new file mode 100644 index 00000000..2072c1c9 --- /dev/null +++ b/test/notes/note-multicluster-subject/chainsaw-test.yaml @@ -0,0 +1,101 @@ +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: note-multicluster-subject +spec: + description: | + Tests that Notes can reference subjects (ConfigMaps) in project control planes. + + Validates: + - Note created in project control plane can reference ConfigMap in same control plane + - Owner reference is correctly set on the Note + - Note is garbage collected when ConfigMap is deleted + + clusters: + main: + kubeconfig: kubeconfig-main + org: + kubeconfig: kubeconfig-org + project: + kubeconfig: kubeconfig-project-1 + + steps: + - name: setup-organization + description: Create test organization + cluster: main + try: + - apply: + file: 01-organization.yaml + - wait: + apiVersion: v1 + kind: Namespace + name: organization-note-mc-test-org + timeout: 30s + for: + jsonPath: + path: '{.status.phase}' + value: Active + + - name: create-project + description: Create project in org control plane + cluster: org + try: + - apply: + file: test-data/project.yaml + - wait: + apiVersion: resourcemanager.miloapis.com/v1alpha1 + kind: Project + name: note-mc-test-project-1 + timeout: 60s + for: + condition: + name: Ready + value: 'true' + + - name: create-configmap-in-project + description: Create ConfigMap resource in project control plane + cluster: project + try: + - apply: + file: test-data/configmap.yaml + - assert: + resource: + apiVersion: v1 + kind: ConfigMap + metadata: + name: test-subject-configmap + namespace: default + + - name: create-note-referencing-configmap + description: Create Note referencing the ConfigMap in project control plane + cluster: project + try: + - apply: + file: test-data/note.yaml + - assert: + resource: + apiVersion: notes.miloapis.com/v1alpha1 + kind: Note + metadata: + name: test-configmap-note + namespace: default + (length(metadata.ownerReferences) > `0`): true + + - name: delete-configmap-verify-note-deletion + description: Delete ConfigMap and verify Note is garbage collected + cluster: project + try: + - delete: + ref: + apiVersion: v1 + kind: ConfigMap + name: test-subject-configmap + namespace: default + - wait: + apiVersion: notes.miloapis.com/v1alpha1 + kind: Note + name: test-configmap-note + namespace: default + timeout: 60s + for: + deletion: {} diff --git a/test/notes/note-multicluster-subject/kubeconfig-main b/test/notes/note-multicluster-subject/kubeconfig-main new file mode 100644 index 00000000..b0496e6c --- /dev/null +++ b/test/notes/note-multicluster-subject/kubeconfig-main @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:30443 + name: milo-test-infra +contexts: +- context: + cluster: milo-test-infra + user: admin + name: milo-test-infra +current-context: milo-test-infra +kind: Config +preferences: {} +users: +- name: admin + user: + token: test-admin-token diff --git a/test/notes/note-multicluster-subject/kubeconfig-org b/test/notes/note-multicluster-subject/kubeconfig-org new file mode 100644 index 00000000..86c9bdb8 --- /dev/null +++ b/test/notes/note-multicluster-subject/kubeconfig-org @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:30443/apis/resourcemanager.miloapis.com/v1alpha1/organizations/note-mc-test-org/control-plane + name: org-note-mc-test-org +contexts: +- context: + cluster: org-note-mc-test-org + user: user-1001 + name: org-note-mc-test-org +current-context: org-note-mc-test-org +kind: Config +preferences: {} +users: +- name: user-1001 + user: + token: test-admin-token diff --git a/test/notes/note-multicluster-subject/kubeconfig-project-1 b/test/notes/note-multicluster-subject/kubeconfig-project-1 new file mode 100644 index 00000000..044a3de1 --- /dev/null +++ b/test/notes/note-multicluster-subject/kubeconfig-project-1 @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:30443/apis/resourcemanager.miloapis.com/v1alpha1/projects/note-mc-test-project-1/control-plane + name: project-note-mc-test-project-1 +contexts: +- context: + cluster: project-note-mc-test-project-1 + user: user-1001 + name: project-note-mc-test-project-1 +current-context: project-note-mc-test-project-1 +kind: Config +preferences: {} +users: +- name: user-1001 + user: + token: test-admin-token diff --git a/test/notes/note-multicluster-subject/test-data/configmap.yaml b/test/notes/note-multicluster-subject/test-data/configmap.yaml new file mode 100644 index 00000000..daf01ab0 --- /dev/null +++ b/test/notes/note-multicluster-subject/test-data/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-subject-configmap + namespace: default + labels: + test.miloapis.com/notes: "multicluster" +data: + key: value diff --git a/test/notes/note-multicluster-subject/test-data/note.yaml b/test/notes/note-multicluster-subject/test-data/note.yaml new file mode 100644 index 00000000..77c424ec --- /dev/null +++ b/test/notes/note-multicluster-subject/test-data/note.yaml @@ -0,0 +1,14 @@ +apiVersion: notes.miloapis.com/v1alpha1 +kind: Note +metadata: + name: test-configmap-note + namespace: default + labels: + test.miloapis.com/notes: "multicluster" +spec: + content: "This note references a ConfigMap in the project control plane" + subjectRef: + apiGroup: "" + kind: ConfigMap + name: test-subject-configmap + namespace: default diff --git a/test/notes/note-multicluster-subject/test-data/project.yaml b/test/notes/note-multicluster-subject/test-data/project.yaml new file mode 100644 index 00000000..d75cd25a --- /dev/null +++ b/test/notes/note-multicluster-subject/test-data/project.yaml @@ -0,0 +1,10 @@ +apiVersion: resourcemanager.miloapis.com/v1alpha1 +kind: Project +metadata: + name: note-mc-test-project-1 + labels: + test.miloapis.com/notes: "multicluster" +spec: + ownerRef: + kind: Organization + name: note-mc-test-org