From 3c569bba28156a92ec8f5f5df24c40cb5d9950a5 Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Tue, 14 Oct 2025 17:04:28 -0300 Subject: [PATCH 01/11] feat: implement DocumentController with reconciliation logic for Document and DocumentRevision resources This commit introduces the DocumentController, which manages the reconciliation of Document objects and their associated DocumentRevisions. It includes logic to update the document's status based on the presence of revisions, ensuring the LatestRevisionRef is correctly set. Additionally, unit tests for the DocumentController's reconciliation process are added to verify functionality. --- .../documentation/document_controller.go | 188 ++++++++++++++++++ .../documentation/document_controller_test.go | 151 ++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 internal/controllers/documentation/document_controller.go create mode 100644 internal/controllers/documentation/document_controller_test.go diff --git a/internal/controllers/documentation/document_controller.go b/internal/controllers/documentation/document_controller.go new file mode 100644 index 00000000..89ed5629 --- /dev/null +++ b/internal/controllers/documentation/document_controller.go @@ -0,0 +1,188 @@ +package documents + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/finalizer" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + docversion "go.miloapis.com/milo/pkg/version" + + documentationv1alpha1 "go.miloapis.com/milo/pkg/apis/documentation/v1alpha1" +) + +const ( + documentRefNamespacedKey = "documentation.miloapis.com/documentnamespacedkey" +) + +func buildDocumentRevisionByDocumentIndexKey(docRef documentationv1alpha1.DocumentReference) string { + return fmt.Sprintf("%s|%s", docRef.Name, docRef.Namespace) +} + +// DocumentController reconciles a Document object +type DocumentController struct { + Client client.Client + Finalizers finalizer.Finalizers +} + +// +kubebuilder:rbac:groups=documentation.miloapis.com,resources=documents,verbs=get;list;watch +// +kubebuilder:rbac:groups=documentation.miloapis.com,resources=documents/status,verbs=update;patch +// +kubebuilder:rbac:groups=documentation.miloapis.com,resources=documentrevisions,verbs=get;list;watch + +func (r *DocumentController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx).WithValues("controller", "DocumentController", "trigger", req.NamespacedName) + log.Info("Starting reconciliation", "namespacedName", req.String(), "name", req.Name, "namespace", req.Namespace) + + // Get document + var document documentationv1alpha1.Document + if err := r.Client.Get(ctx, req.NamespacedName, &document); err != nil { + if errors.IsNotFound(err) { + log.Info("Document not found. Probably deleted.") + return ctrl.Result{}, nil + } + log.Error(err, "failed to get document") + return ctrl.Result{}, err + } + oldStatus := document.Status.DeepCopy() + + // Get document revisions + var documentRevisions documentationv1alpha1.DocumentRevisionList + if err := r.Client.List(ctx, &documentRevisions, + client.MatchingFields{ + documentRefNamespacedKey: buildDocumentRevisionByDocumentIndexKey( + documentationv1alpha1.DocumentReference{Name: document.Name, Namespace: document.Namespace})}); err != nil { + log.Error(err, "failed to get document revisions") + return ctrl.Result{}, err + } + // Verify if there is at least one revision + revisionFound := false + if len(documentRevisions.Items) > 0 { + revisionFound = true + } + + // Update document status + meta.SetStatusCondition(&document.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Reconciled", + Message: "Document reconciled", + ObservedGeneration: document.Generation, + }) + if revisionFound { + log.Info("Revision found. Updating latest revision status reference") + latestRevision, err := GetLatestDocumentRevision(documentRevisions) + if err != nil { + log.Error(err, "failed to get latest document revision") + return ctrl.Result{}, err + } + // Update latest revision status reference + document.Status.LatestRevisionRef = &documentationv1alpha1.LatestRevisionRef{ + Name: latestRevision.Name, + Namespace: latestRevision.Namespace, + Version: latestRevision.Spec.Version, + PublishedAt: latestRevision.Spec.EffectiveDate, + } + } else { + log.Info("No revision found. Updating latest revision status reference to nil") + document.Status.LatestRevisionRef = nil + } + // Update document status onlyif it changed + if !equality.Semantic.DeepEqual(oldStatus, &document.Status) { + if err := r.Client.Status().Update(ctx, &document); err != nil { + log.Error(err, "Failed to update document status") + return ctrl.Result{}, fmt.Errorf("failed to update document status: %w", err) + } + } else { + log.V(1).Info("Document status unchanged, skipping update") + } + + log.Info("Document reconciled") + + return ctrl.Result{}, nil +} + +// GetLatestDocumentRevision returns the latest document revision from the list of document revisions. +func GetLatestDocumentRevision(documentRevisions documentationv1alpha1.DocumentRevisionList) (documentationv1alpha1.DocumentRevision, error) { + latestRevision := documentRevisions.Items[0] + for _, revision := range documentRevisions.Items { + isHigher, err := docversion.IsVersionHigher(revision.Spec.Version, latestRevision.Spec.Version) + if err != nil { + return documentationv1alpha1.DocumentRevision{}, err + } + if isHigher { + latestRevision = revision + } + } + + return latestRevision, nil +} + +// enqueueDocumentForDocumentRevisionCreate enqueues the referenced document for the document revision create event. +// This is used to ensure that the document status is updated when a document revision is created. +func (r *DocumentController) enqueueDocumentForDocumentRevisionCreate(ctx context.Context, obj client.Object) []ctrl.Request { + log := logf.FromContext(ctx).WithValues("controller", "DocumentController", "trigger", obj.GetName()) + log.Info("Enqueuing document for document revision create") + dr, ok := obj.(*documentationv1alpha1.DocumentRevision) + if !ok { + log.Error(fmt.Errorf("failed to cast object to DocumentRevision"), "failed to cast object to DocumentRevision") + return nil + } + + referencedDocument := &documentationv1alpha1.Document{} + if err := r.Client.Get(ctx, client.ObjectKey{Namespace: dr.Spec.DocumentRef.Namespace, Name: dr.Spec.DocumentRef.Name}, referencedDocument); err != nil { + // Document must exists, as webhook validates it + log.Error(err, "failed to get referenced document", "namespace", dr.Spec.DocumentRef.Namespace, "name", dr.Spec.DocumentRef.Name) + return nil + } + log.V(1).Info("Referenced document found. Enqueuing document", "namespace", referencedDocument.Namespace, "name", referencedDocument.Name) + + return []ctrl.Request{ + { + NamespacedName: types.NamespacedName{ + Name: referencedDocument.Name, + Namespace: referencedDocument.Namespace, + }, + }, + } +} + +func (r *DocumentController) SetupWithManager(mgr ctrl.Manager) error { + // Index DocumentRevision by documentref namespaced key for efficient lookups + if err := mgr.GetFieldIndexer().IndexField(context.Background(), + &documentationv1alpha1.DocumentRevision{}, documentRefNamespacedKey, func(obj client.Object) []string { + dr, ok := obj.(*documentationv1alpha1.DocumentRevision) + if !ok { + return nil + } + return []string{buildDocumentRevisionByDocumentIndexKey(dr.Spec.DocumentRef)} + }); err != nil { + return fmt.Errorf("failed to set field index on DocumentRevision: %w", err) + } + + return ctrl.NewControllerManagedBy(mgr). + For(&documentationv1alpha1.Document{}). + Watches( + &documentationv1alpha1.DocumentRevision{}, + handler.EnqueueRequestsFromMapFunc(r.enqueueDocumentForDocumentRevisionCreate), + builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { return true }, + DeleteFunc: func(e event.DeleteEvent) bool { return false }, + GenericFunc: func(e event.GenericEvent) bool { return false }, + UpdateFunc: func(e event.UpdateEvent) bool { return false }, + }), + ). + Named("document"). + Complete(r) +} diff --git a/internal/controllers/documentation/document_controller_test.go b/internal/controllers/documentation/document_controller_test.go new file mode 100644 index 00000000..7009a32d --- /dev/null +++ b/internal/controllers/documentation/document_controller_test.go @@ -0,0 +1,151 @@ +package documents + +import ( + "context" + "testing" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/finalizer" + + documentationv1alpha1 "go.miloapis.com/milo/pkg/apis/documentation/v1alpha1" +) + +func TestDocumentController_Reconcile_LatestRevisionRefUpdated(t *testing.T) { + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add client-go scheme: %v", err) + } + if err := documentationv1alpha1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add documentation scheme: %v", err) + } + + // Build fake client that supports status subresource updates + fakeClient := fake.NewClientBuilder().WithScheme(scheme). + WithStatusSubresource(&documentationv1alpha1.Document{}). + WithIndex(&documentationv1alpha1.DocumentRevision{}, documentRefNamespacedKey, func(obj client.Object) []string { + dr, ok := obj.(*documentationv1alpha1.DocumentRevision) + if !ok { + return nil + } + return []string{buildDocumentRevisionByDocumentIndexKey(dr.Spec.DocumentRef)} + }). + Build() + + ctx := context.TODO() + + // Create a sample Document in the fake cluster + doc := &documentationv1alpha1.Document{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample-document", + Namespace: "default", + }, + Spec: documentationv1alpha1.DocumentSpec{ + Title: "TOS", + Description: "Terms of Service", + DocumentType: "tos", + }, + Metadata: documentationv1alpha1.DocumentMetadata{ + Category: "legal", + Jurisdiction: "us", + }, + } + if err := fakeClient.Create(ctx, doc); err != nil { + t.Fatalf("failed to create document: %v", err) + } + + r := &DocumentController{ + Client: fakeClient, + Finalizers: finalizer.NewFinalizers(), + } + + req := ctrl.Request{NamespacedName: client.ObjectKey{Name: doc.Name, Namespace: doc.Namespace}} + + // First reconcile: no revisions exist yet -> LatestRevisionRef should stay nil + if _, err := r.Reconcile(ctx, req); err != nil { + t.Fatalf("reconcile failed: %v", err) + } + if err := fakeClient.Get(ctx, req.NamespacedName, doc); err != nil { + t.Fatalf("failed to get document after reconcile: %v", err) + } + if doc.Status.LatestRevisionRef != nil { + t.Fatalf("expected LatestRevisionRef to be nil, got %v", doc.Status.LatestRevisionRef) + } + + // Verify Ready condition is present and set to True with reason Reconciled + cond := meta.FindStatusCondition(doc.Status.Conditions, "Ready") + if cond == nil { + t.Fatalf("expected Ready condition to be present") + } + if cond.Status != metav1.ConditionTrue || cond.Reason != "Reconciled" { + t.Fatalf("unexpected Ready condition: %+v", cond) + } + + // Create first DocumentRevision v1.0.0 + rev1 := &documentationv1alpha1.DocumentRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample-document-v1", + Namespace: "default", + }, + Spec: documentationv1alpha1.DocumentRevisionSpec{ + DocumentRef: documentationv1alpha1.DocumentReference{Name: doc.Name, Namespace: doc.Namespace}, + Version: "v1.0.0", + EffectiveDate: metav1.Time{Time: time.Now()}, + Content: documentationv1alpha1.DocumentRevisionContent{Format: "markdown", Data: "initial content"}, + ChangesSummary: "initial version", + ExpectedSubjectKinds: []documentationv1alpha1.DocumentRevisionExpectedSubjectKind{{APIGroup: "", Kind: ""}}, + ExpectedAccepterKinds: []documentationv1alpha1.DocumentRevisionExpectedAccepterKind{{APIGroup: "iam.miloapis.com", Kind: "User"}}, + }, + } + if err := fakeClient.Create(ctx, rev1); err != nil { + t.Fatalf("failed to create document revision 1: %v", err) + } + + // Reconcile again -> LatestRevisionRef should be updated to v1.0.0 + if _, err := r.Reconcile(ctx, req); err != nil { + t.Fatalf("reconcile failed: %v", err) + } + if err := fakeClient.Get(ctx, req.NamespacedName, doc); err != nil { + t.Fatalf("failed to get document after reconcile 2: %v", err) + } + if doc.Status.LatestRevisionRef == nil || string(doc.Status.LatestRevisionRef.Version) != "v1.0.0" { + t.Fatalf("expected LatestRevisionRef version v1.0.0, got %v", doc.Status.LatestRevisionRef) + } + + // Create second DocumentRevision v1.1.0 (higher) + rev2 := &documentationv1alpha1.DocumentRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample-document-v2", + Namespace: "default", + }, + Spec: documentationv1alpha1.DocumentRevisionSpec{ + DocumentRef: documentationv1alpha1.DocumentReference{Name: doc.Name, Namespace: doc.Namespace}, + Version: "v1.1.0", + EffectiveDate: metav1.Time{Time: time.Now()}, + Content: documentationv1alpha1.DocumentRevisionContent{Format: "markdown", Data: "updated content"}, + ChangesSummary: "update", + ExpectedSubjectKinds: []documentationv1alpha1.DocumentRevisionExpectedSubjectKind{{APIGroup: "", Kind: ""}}, + ExpectedAccepterKinds: []documentationv1alpha1.DocumentRevisionExpectedAccepterKind{{APIGroup: "iam.miloapis.com", Kind: "User"}}, + }, + } + if err := fakeClient.Create(ctx, rev2); err != nil { + t.Fatalf("failed to create document revision 2: %v", err) + } + + // Reconcile again -> LatestRevisionRef should be updated to v1.1.0 + if _, err := r.Reconcile(ctx, req); err != nil { + t.Fatalf("reconcile failed: %v", err) + } + if err := fakeClient.Get(ctx, req.NamespacedName, doc); err != nil { + t.Fatalf("failed to get document after reconcile 3: %v", err) + } + if doc.Status.LatestRevisionRef == nil || string(doc.Status.LatestRevisionRef.Version) != "v1.1.0" { + t.Fatalf("expected LatestRevisionRef version v1.1.0, got %v", doc.Status.LatestRevisionRef.Version) + } +} From a8860559fb8de9ef91a44ecebda8b7eabc06fe15 Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Wed, 15 Oct 2025 09:55:15 -0300 Subject: [PATCH 02/11] feat: add DocumentRevisionController with reconciliation logic and unit tests This commit introduces the DocumentRevisionController, which manages the reconciliation of DocumentRevision resources. It includes logic to calculate and persist a content hash, ensuring that updates to the document's content do not change the hash if the document is already reconciled. Additionally, unit tests are added to verify the reconciliation behavior of the controller. --- .../documentrevision_controller.go | 72 ++++++++++++++ .../documentrevision_controller_test.go | 98 +++++++++++++++++++ pkg/util/hash/hash.go | 12 +++ 3 files changed, 182 insertions(+) create mode 100644 internal/controllers/documentation/documentrevision_controller.go create mode 100644 internal/controllers/documentation/documentrevision_controller_test.go create mode 100644 pkg/util/hash/hash.go diff --git a/internal/controllers/documentation/documentrevision_controller.go b/internal/controllers/documentation/documentrevision_controller.go new file mode 100644 index 00000000..d57ddc28 --- /dev/null +++ b/internal/controllers/documentation/documentrevision_controller.go @@ -0,0 +1,72 @@ +package documents + +import ( + "context" + "fmt" + + documentationv1alpha1 "go.miloapis.com/milo/pkg/apis/documentation/v1alpha1" + "go.miloapis.com/milo/pkg/util/hash" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +// DocumentRevisionController reconciles a DocumentRevision object +type DocumentRevisionController struct { + Client client.Client +} + +// +kubebuilder:rbac:groups=documentation.miloapis.com,resources=documentrevisions,verbs=get;list;watch +// +kubebuilder:rbac:groups=documentation.miloapis.com,resources=documentrevisions/status,verbs=update;patch + +func (r *DocumentRevisionController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx).WithValues("controller", "DocumentRevisionController", "trigger", req.NamespacedName) + log.Info("Starting reconciliation", "namespacedName", req.String(), "name", req.Name, "namespace", req.Namespace) + + // Get document revision + var documentRevision documentationv1alpha1.DocumentRevision + if err := r.Client.Get(ctx, req.NamespacedName, &documentRevision); err != nil { + if errors.IsNotFound(err) { + log.Info("Document revision not found. Probably deleted.") + return ctrl.Result{}, nil + } + log.Error(err, "failed to get document revision") + return ctrl.Result{}, err + } + + // DocumentRevision does not allows updates to its status, as the hash is used + // to detect if the content of the document revision has changed. + if meta.IsStatusConditionTrue(documentRevision.Status.Conditions, "Ready") { + log.Info("Document revision already reconciled") + return ctrl.Result{}, nil + } + + documentRevision.Status.ContentHash = hash.SHA256Hex(documentRevision.Spec.Content.Data) + + // Update document revision status + meta.SetStatusCondition(&documentRevision.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Reconciled", + Message: "Document revision reconciled", + ObservedGeneration: documentRevision.Generation, + }) + if err := r.Client.Status().Update(ctx, &documentRevision); err != nil { + log.Error(err, "Failed to update document revision status") + return ctrl.Result{}, fmt.Errorf("failed to update document revision status: %w", err) + } + + log.Info("Document revision reconciled") + + return ctrl.Result{}, nil +} + +func (r *DocumentRevisionController) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&documentationv1alpha1.DocumentRevision{}). + Named("documentrevision"). + Complete(r) +} diff --git a/internal/controllers/documentation/documentrevision_controller_test.go b/internal/controllers/documentation/documentrevision_controller_test.go new file mode 100644 index 00000000..0891d7ef --- /dev/null +++ b/internal/controllers/documentation/documentrevision_controller_test.go @@ -0,0 +1,98 @@ +package documents + +import ( + "context" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + documentationv1alpha1 "go.miloapis.com/milo/pkg/apis/documentation/v1alpha1" + "go.miloapis.com/milo/pkg/util/hash" +) + +func TestDocumentRevisionController_Reconcile_HashBehaviour(t *testing.T) { + scheme := runtime.NewScheme() + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add client-go scheme: %v", err) + } + if err := documentationv1alpha1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add documentation scheme: %v", err) + } + + // Build fake client that supports status subresource updates + fakeClient := fake.NewClientBuilder().WithScheme(scheme). + WithStatusSubresource(&documentationv1alpha1.DocumentRevision{}). + Build() + + ctx := context.TODO() + + // Create a sample Document in the fake cluster (needed to satisfy reference, controller doesn't use it) + doc := &documentationv1alpha1.Document{ + ObjectMeta: metav1.ObjectMeta{Name: "sample-document", Namespace: "default"}, + Spec: documentationv1alpha1.DocumentSpec{ + Title: "TOS", Description: "Terms", DocumentType: "tos", + }, + Metadata: documentationv1alpha1.DocumentMetadata{Category: "legal", Jurisdiction: "us"}, + } + if err := fakeClient.Create(ctx, doc); err != nil { + t.Fatalf("failed to create document: %v", err) + } + + // Initial content + initialContent := "initial content" + + rev := &documentationv1alpha1.DocumentRevision{ + ObjectMeta: metav1.ObjectMeta{Name: "sample-document-v1", Namespace: "default"}, + Spec: documentationv1alpha1.DocumentRevisionSpec{ + DocumentRef: documentationv1alpha1.DocumentReference{Name: doc.Name, Namespace: doc.Namespace}, + Version: "v1.0.0", + EffectiveDate: metav1.Now(), + Content: documentationv1alpha1.DocumentRevisionContent{Format: "markdown", Data: initialContent}, + ChangesSummary: "initial", + ExpectedSubjectKinds: []documentationv1alpha1.DocumentRevisionExpectedSubjectKind{{APIGroup: "", Kind: ""}}, + ExpectedAccepterKinds: []documentationv1alpha1.DocumentRevisionExpectedAccepterKind{{APIGroup: "iam.miloapis.com", Kind: "User"}}, + }, + } + if err := fakeClient.Create(ctx, rev); err != nil { + t.Fatalf("failed to create document revision: %v", err) + } + + r := &DocumentRevisionController{Client: fakeClient} + + req := ctrl.Request{NamespacedName: client.ObjectKey{Name: rev.Name, Namespace: rev.Namespace}} + + // First reconcile should calculate and persist hash + if _, err := r.Reconcile(ctx, req); err != nil { + t.Fatalf("reconcile failed: %v", err) + } + if err := fakeClient.Get(ctx, req.NamespacedName, rev); err != nil { + t.Fatalf("failed to get revision after reconcile: %v", err) + } + expectedHash := hash.SHA256Hex(initialContent) + if rev.Status.ContentHash != expectedHash { + t.Fatalf("expected content hash %s, got %s", expectedHash, rev.Status.ContentHash) + } + + // Modify spec content (controller should ignore because Ready=True) + updatedContent := "modified content" + rev.Spec.Content.Data = updatedContent + if err := fakeClient.Update(ctx, rev); err != nil { + t.Fatalf("failed to update revision content: %v", err) + } + + // Reconcile again - hash should remain unchanged + if _, err := r.Reconcile(ctx, req); err != nil { + t.Fatalf("reconcile 2 failed: %v", err) + } + if err := fakeClient.Get(ctx, req.NamespacedName, rev); err != nil { + t.Fatalf("failed to get revision after reconcile 2: %v", err) + } + if rev.Status.ContentHash != expectedHash { + t.Fatalf("content hash changed unexpectedly: expected %s got %s", expectedHash, rev.Status.ContentHash) + } +} diff --git a/pkg/util/hash/hash.go b/pkg/util/hash/hash.go new file mode 100644 index 00000000..94e500ce --- /dev/null +++ b/pkg/util/hash/hash.go @@ -0,0 +1,12 @@ +package hash + +import ( + "crypto/sha256" + "encoding/hex" +) + +// SHA256Hex returns the hex-encoded SHA-256 hash of the given string. +func SHA256Hex(data string) string { + h := sha256.Sum256([]byte(data)) + return hex.EncodeToString(h[:]) +} From e9bae54fbd8977a02eff0a7877017a77b98019e2 Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Wed, 15 Oct 2025 10:12:59 -0300 Subject: [PATCH 03/11] feat: integrate Document and DocumentRevision controllers into the controller manager --- .../controller-manager/controllermanager.go | 17 +++++++++++++++++ .../overlays/core-control-plane/rbac/role.yaml | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/cmd/milo/controller-manager/controllermanager.go b/cmd/milo/controller-manager/controllermanager.go index 09f660e6..458ed2f6 100644 --- a/cmd/milo/controller-manager/controllermanager.go +++ b/cmd/milo/controller-manager/controllermanager.go @@ -74,6 +74,7 @@ import ( // Datum webhook and API type imports controlplane "go.miloapis.com/milo/internal/control-plane" + documentationcontroller "go.miloapis.com/milo/internal/controllers/documentation" iamcontroller "go.miloapis.com/milo/internal/controllers/iam" remoteapiservicecontroller "go.miloapis.com/milo/internal/controllers/remoteapiservice" resourcemanagercontroller "go.miloapis.com/milo/internal/controllers/resourcemanager" @@ -506,6 +507,22 @@ func Run(ctx context.Context, c *config.CompletedConfig, opts *Options) error { klog.FlushAndExit(klog.ExitFlushTimeout, 1) } + documentCtrl := documentationcontroller.DocumentController{ + Client: ctrl.GetClient(), + } + if err := documentCtrl.SetupWithManager(ctrl); err != nil { + logger.Error(err, "Error setting up document controller") + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } + + documentRevisionCtrl := documentationcontroller.DocumentRevisionController{ + Client: ctrl.GetClient(), + } + if err := documentRevisionCtrl.SetupWithManager(ctrl); err != nil { + logger.Error(err, "Error setting up document revision controller") + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } + userInvitationCtrl := iamcontroller.UserInvitationController{ Client: ctrl.GetClient(), SystemNamespace: SystemNamespace, diff --git a/config/controller-manager/overlays/core-control-plane/rbac/role.yaml b/config/controller-manager/overlays/core-control-plane/rbac/role.yaml index acfa9d3c..2e1a4ff6 100644 --- a/config/controller-manager/overlays/core-control-plane/rbac/role.yaml +++ b/config/controller-manager/overlays/core-control-plane/rbac/role.yaml @@ -31,6 +31,23 @@ rules: verbs: - patch - update +- apiGroups: + - documentation.miloapis.com + resources: + - documentrevisions + - documents + verbs: + - get + - list + - watch +- apiGroups: + - documentation.miloapis.com + resources: + - documentrevisions/status + - documents/status + verbs: + - patch + - update - apiGroups: - gateway.networking.k8s.io resources: From 9edbc7cc3d363f5a7a02374a1d8011e73a137b33 Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Thu, 16 Oct 2025 11:59:38 -0300 Subject: [PATCH 04/11] fix: add Agreement API to controller manager initialization --- cmd/milo/controller-manager/controllermanager.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/milo/controller-manager/controllermanager.go b/cmd/milo/controller-manager/controllermanager.go index 458ed2f6..62b94c39 100644 --- a/cmd/milo/controller-manager/controllermanager.go +++ b/cmd/milo/controller-manager/controllermanager.go @@ -83,6 +83,7 @@ import ( iamv1alpha1webhook "go.miloapis.com/milo/internal/webhooks/iam/v1alpha1" notificationv1alpha1webhook "go.miloapis.com/milo/internal/webhooks/notification/v1alpha1" resourcemanagerv1alpha1webhook "go.miloapis.com/milo/internal/webhooks/resourcemanager/v1alpha1" + agreementv1alpha1 "go.miloapis.com/milo/pkg/apis/agreement/v1alpha1" documentationv1alpha1 "go.miloapis.com/milo/pkg/apis/documentation/v1alpha1" iamv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1" infrastructurev1alpha1 "go.miloapis.com/milo/pkg/apis/infrastructure/v1alpha1" @@ -133,6 +134,7 @@ func init() { utilruntime.Must(notificationv1alpha1.AddToScheme(Scheme)) utilruntime.Must(documentationv1alpha1.AddToScheme(Scheme)) utilruntime.Must(apiregistrationv1.AddToScheme(Scheme)) + utilruntime.Must(agreementv1alpha1.AddToScheme(Scheme)) } const ( From ab9708ce78509574364c86cd1f765baf8b409b54 Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Thu, 16 Oct 2025 12:27:21 -0300 Subject: [PATCH 05/11] fix: update kustomization to include Agreement and Documentation bases --- config/crd/overlays/core-control-plane/kustomization.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/crd/overlays/core-control-plane/kustomization.yaml b/config/crd/overlays/core-control-plane/kustomization.yaml index 8ae8e5bf..aafb11d1 100644 --- a/config/crd/overlays/core-control-plane/kustomization.yaml +++ b/config/crd/overlays/core-control-plane/kustomization.yaml @@ -2,3 +2,5 @@ resources: - ../../bases/iam/ - ../../bases/resourcemanager/ - ../../bases/notification/ +- ../../bases/agreement/ +- ../../bases/documentation/ From cbcf028d7f02b5c8edcca0ecec2e46153e0c7fb6 Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Thu, 16 Oct 2025 14:03:52 -0300 Subject: [PATCH 06/11] fix: update webhook paths for document and documentrevision resources --- config/webhook/manifests.yaml | 4 ++-- internal/webhooks/documentation/v1alpha1/document_webhook.go | 2 +- .../documentation/v1alpha1/documentrevision_webhook.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 7ccaa9e7..a21b6b11 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -123,7 +123,7 @@ webhooks: service: name: milo-controller-manager namespace: milo-system - path: /validate-documentation-miloapis-com-v1alpha1-documentation + path: /validate-documentation-miloapis-com-v1alpha1-document port: 9443 failurePolicy: Fail name: vdocument.documentation.miloapis.com @@ -144,7 +144,7 @@ webhooks: service: name: milo-controller-manager namespace: milo-system - path: /validate-documentation-miloapis-com-v1alpha1-documentation + path: /validate-documentation-miloapis-com-v1alpha1-documentrevision port: 9443 failurePolicy: Fail name: vdocumentrevision.documentation.miloapis.com diff --git a/internal/webhooks/documentation/v1alpha1/document_webhook.go b/internal/webhooks/documentation/v1alpha1/document_webhook.go index 7a631420..9840676c 100644 --- a/internal/webhooks/documentation/v1alpha1/document_webhook.go +++ b/internal/webhooks/documentation/v1alpha1/document_webhook.go @@ -29,7 +29,7 @@ func SetupDocumentWebhooksWithManager(mgr ctrl.Manager) error { Complete() } -// +kubebuilder:webhook:path=/validate-documentation-miloapis-com-v1alpha1-documentation,mutating=false,failurePolicy=fail,sideEffects=None,groups=documentation.miloapis.com,resources=documents,verbs=delete,versions=v1alpha1,name=vdocument.documentation.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system +// +kubebuilder:webhook:path=/validate-documentation-miloapis-com-v1alpha1-document,mutating=false,failurePolicy=fail,sideEffects=None,groups=documentation.miloapis.com,resources=documents,verbs=delete,versions=v1alpha1,name=vdocument.documentation.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system type DocumentValidator struct { Client client.Client diff --git a/internal/webhooks/documentation/v1alpha1/documentrevision_webhook.go b/internal/webhooks/documentation/v1alpha1/documentrevision_webhook.go index 0ff2d1f6..b6097b72 100644 --- a/internal/webhooks/documentation/v1alpha1/documentrevision_webhook.go +++ b/internal/webhooks/documentation/v1alpha1/documentrevision_webhook.go @@ -33,7 +33,7 @@ func SetupDocumentRevisionWebhooksWithManager(mgr ctrl.Manager) error { Complete() } -// +kubebuilder:webhook:path=/validate-documentation-miloapis-com-v1alpha1-documentation,mutating=false,failurePolicy=fail,sideEffects=None,groups=documentation.miloapis.com,resources=documentrevisions,verbs=delete;create,versions=v1alpha1,name=vdocumentrevision.documentation.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system +// +kubebuilder:webhook:path=/validate-documentation-miloapis-com-v1alpha1-documentrevision,mutating=false,failurePolicy=fail,sideEffects=None,groups=documentation.miloapis.com,resources=documentrevisions,verbs=delete;create,versions=v1alpha1,name=vdocumentrevision.documentation.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system type DocumentRevisionValidator struct { Client client.Client From 0796422fd0eae100e145a2dcfaec1e947217e28b Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Thu, 16 Oct 2025 23:42:26 -0300 Subject: [PATCH 07/11] fix: enhance UserInvitationMutator to retrieve inviter user details from the client --- .../iam/v1alpha1/userinvitation_webhook.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/webhooks/iam/v1alpha1/userinvitation_webhook.go b/internal/webhooks/iam/v1alpha1/userinvitation_webhook.go index f03f5828..df1a6a91 100644 --- a/internal/webhooks/iam/v1alpha1/userinvitation_webhook.go +++ b/internal/webhooks/iam/v1alpha1/userinvitation_webhook.go @@ -26,7 +26,9 @@ func SetupUserInvitationWebhooksWithManager(mgr ctrl.Manager, systemNamespace st return ctrl.NewWebhookManagedBy(mgr). For(&iamv1alpha1.UserInvitation{}). - WithDefaulter(&UserInvitationMutator{}). + WithDefaulter(&UserInvitationMutator{ + client: mgr.GetClient(), + }). WithValidator(&UserInvitationValidator{ client: mgr.GetClient(), systemNamespace: systemNamespace, @@ -37,7 +39,9 @@ func SetupUserInvitationWebhooksWithManager(mgr ctrl.Manager, systemNamespace st // +kubebuilder:webhook:path=/mutate-iam-miloapis-com-v1alpha1-userinvitation,mutating=true,failurePolicy=fail,sideEffects=None,groups=iam.miloapis.com,resources=userinvitations,verbs=create,versions=v1alpha1,name=muserinvitation.iam.miloapis.com,admissionReviewVersions={v1,v1beta1},serviceName=milo-controller-manager,servicePort=9443,serviceNamespace=milo-system // UserInvitationMutator sets default values for UserInvitation resources. -type UserInvitationMutator struct{} +type UserInvitationMutator struct { + client client.Client +} // Default sets the InvitedBy field to the requesting user if not already set. func (m *UserInvitationMutator) Default(ctx context.Context, obj runtime.Object) error { @@ -52,8 +56,14 @@ func (m *UserInvitationMutator) Default(ctx context.Context, obj runtime.Object) return fmt.Errorf("failed to get request from context: %w", err) } + inviterUser := &iamv1alpha1.User{} + if err := m.client.Get(ctx, client.ObjectKey{Name: string(req.UserInfo.UID)}, inviterUser); err != nil { + userinvitationlog.Error(err, "failed to get user '%s' from iam.miloapis.com API", string(req.UserInfo.UID)) + return errors.NewInternalError(fmt.Errorf("failed to get user '%s' from iam.miloapis.com API: %w", string(req.UserInfo.UID), err)) + } + ui.Spec.InvitedBy = iamv1alpha1.UserReference{ - Name: req.UserInfo.Username, + Name: inviterUser.Name, } return nil From 75fb8a3b8088d81ff47a18c8f65ca6c4884f625d Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Fri, 17 Oct 2025 11:16:53 -0300 Subject: [PATCH 08/11] fix: update ContactMutator to handle SubjectRef changes and adjust owner references during updates --- .../notification/v1alpha1/contact_webhook.go | 26 ++++-- .../v1alpha1/contact_webhook_test.go | 82 ++++++++++++++++++- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/internal/webhooks/notification/v1alpha1/contact_webhook.go b/internal/webhooks/notification/v1alpha1/contact_webhook.go index 1d6d0aa6..6255717b 100644 --- a/internal/webhooks/notification/v1alpha1/contact_webhook.go +++ b/internal/webhooks/notification/v1alpha1/contact_webhook.go @@ -2,6 +2,7 @@ package v1alpha1 import ( "context" + "encoding/json" "fmt" "net/mail" "reflect" @@ -19,6 +20,7 @@ import ( iamv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1" resourcemanagerv1alpha1 "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" + admissionv1 "k8s.io/api/admission/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -57,6 +59,25 @@ func (m *ContactMutator) Default(ctx context.Context, obj runtime.Object) error } contactLog.Info("Defaulting Contact", "name", contact.Name) + // Detect if this is an update and SubjectRef changed so that we have to adjust ownerReferences + req, err := admission.RequestFromContext(ctx) + if err != nil { + contactLog.Error(err, "failed to get admission request from context", "name", contact.GetName()) + return errors.NewInternalError(fmt.Errorf("failed to get admission request from context: %w", err)) + } + + if req.Operation == admissionv1.Update && len(req.OldObject.Raw) > 0 { + var oldContact notificationv1alpha1.Contact + if err := json.Unmarshal(req.OldObject.Raw, &oldContact); err != nil { + contactLog.Error(err, "failed to unmarshal old object", "name", contact.GetName()) + return errors.NewInternalError(fmt.Errorf("failed to unmarshal old contact object: %w", err)) + } + if !reflect.DeepEqual(oldContact.Spec.SubjectRef, contact.Spec.SubjectRef) { + // SubjectRef changed: remove all existing owner references. + contact.SetOwnerReferences(nil) + } + } + if contact.Spec.SubjectRef != nil { if contact.Spec.SubjectRef.APIGroup == "iam.miloapis.com" { if contact.Spec.SubjectRef.Kind == "User" { @@ -201,11 +222,6 @@ func (v *ContactValidator) ValidateUpdate(ctx context.Context, oldObj, newObj ru } errs := field.ErrorList{} - // If the SubjectRef changed, reject the update. - if !reflect.DeepEqual(contactNew.Spec.SubjectRef, contactOld.Spec.SubjectRef) { - errs = append(errs, field.Invalid(field.NewPath("spec", "subjectRef"), contactNew.Spec.SubjectRef, "subjectRef is immutable and cannot be updated")) - } - // Validate Email format if contactNew.Spec.Email != contactOld.Spec.Email { if _, err := mail.ParseAddress(contactNew.Spec.Email); err != nil { diff --git a/internal/webhooks/notification/v1alpha1/contact_webhook_test.go b/internal/webhooks/notification/v1alpha1/contact_webhook_test.go index b33100fa..080f0d18 100644 --- a/internal/webhooks/notification/v1alpha1/contact_webhook_test.go +++ b/internal/webhooks/notification/v1alpha1/contact_webhook_test.go @@ -2,6 +2,7 @@ package v1alpha1 import ( "context" + "encoding/json" "strings" "testing" @@ -9,12 +10,14 @@ import ( iamv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1" notificationv1alpha1 "go.miloapis.com/milo/pkg/apis/notification/v1alpha1" resourcemanagerv1alpha1 "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" + admissionv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) var runtimeScheme = runtime.NewScheme() @@ -54,7 +57,15 @@ func TestContactMutator_Default(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(runtimeScheme).WithObjects(user).Build() mutator := &ContactMutator{client: fakeClient, scheme: runtimeScheme} - err := mutator.Default(context.Background(), contact) + // Build Admission context simulating a CREATE operation + req := admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Create, + }, + } + ctx := admission.NewContextWithRequest(context.Background(), req) + + err := mutator.Default(ctx, contact) assert.NoError(t, err, "mutator should not return error") // Expect owner reference to be set @@ -67,6 +78,75 @@ func TestContactMutator_Default(t *testing.T) { } } +func TestContactMutator_Update_ChangesOwnerReference(t *testing.T) { + // Prepare old and new Users + oldUser := &iamv1alpha1.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "old-user", + UID: types.UID("uid-old-user"), + }, + Spec: iamv1alpha1.UserSpec{Email: "old@example.com"}, + } + + newUser := &iamv1alpha1.User{ + ObjectMeta: metav1.ObjectMeta{ + Name: "new-user", + UID: types.UID("uid-new-user"), + }, + Spec: iamv1alpha1.UserSpec{Email: "new@example.com"}, + } + + // Old persisted Contact referencing oldUser + oldContact := ¬ificationv1alpha1.Contact{ + ObjectMeta: metav1.ObjectMeta{Name: "contact-update"}, + Spec: notificationv1alpha1.ContactSpec{ + GivenName: "Update", + FamilyName: "Test", + Email: "update@example.com", + SubjectRef: ¬ificationv1alpha1.SubjectReference{ + APIGroup: "iam.miloapis.com", + Kind: "User", + Name: oldUser.Name, + }, + }, + } + // Simulate existing owner reference pointing to oldUser + oldContact.OwnerReferences = []metav1.OwnerReference{{ + APIVersion: iamv1alpha1.SchemeGroupVersion.String(), + Kind: "User", + Name: oldUser.Name, + UID: oldUser.UID, + }} + + // New object sent in update, subjectRef changed to newUser and still carries old owner reference + newContact := oldContact.DeepCopy() + newContact.Spec.SubjectRef.Name = newUser.Name + // ownerReferences still contains old ref; mutator should replace it + + // Fake client with both users + fakeClient := fake.NewClientBuilder().WithScheme(runtimeScheme).WithObjects(oldUser, newUser).Build() + mutator := &ContactMutator{client: fakeClient, scheme: runtimeScheme} + + // Build admission update context + oldRaw, _ := json.Marshal(oldContact) + req := admission.Request{ + AdmissionRequest: admissionv1.AdmissionRequest{ + Operation: admissionv1.Update, + OldObject: runtime.RawExtension{Raw: oldRaw}, + }, + } + ctx := admission.NewContextWithRequest(context.Background(), req) + + err := mutator.Default(ctx, newContact) + assert.NoError(t, err) + + if assert.Len(t, newContact.OwnerReferences, 1, "should have exactly one owner reference after mutation") { + ref := newContact.OwnerReferences[0] + assert.Equal(t, newUser.Name, ref.Name) + assert.Equal(t, newUser.UID, ref.UID) + } +} + func TestContactValidator_ValidateCreate(t *testing.T) { tests := map[string]struct { contact *notificationv1alpha1.Contact From debad54bd777f7c7afb7f03a500dcaf2fb9b607b Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Fri, 17 Oct 2025 12:53:07 -0300 Subject: [PATCH 09/11] Revert "fix: update ContactMutator to handle SubjectRef changes and adjust owner references during updates" This reverts commit 75fb8a3b8088d81ff47a18c8f65ca6c4884f625d. --- .../notification/v1alpha1/contact_webhook.go | 26 ++---- .../v1alpha1/contact_webhook_test.go | 82 +------------------ 2 files changed, 6 insertions(+), 102 deletions(-) diff --git a/internal/webhooks/notification/v1alpha1/contact_webhook.go b/internal/webhooks/notification/v1alpha1/contact_webhook.go index 6255717b..1d6d0aa6 100644 --- a/internal/webhooks/notification/v1alpha1/contact_webhook.go +++ b/internal/webhooks/notification/v1alpha1/contact_webhook.go @@ -2,7 +2,6 @@ package v1alpha1 import ( "context" - "encoding/json" "fmt" "net/mail" "reflect" @@ -20,7 +19,6 @@ import ( iamv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1" resourcemanagerv1alpha1 "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" - admissionv1 "k8s.io/api/admission/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -59,25 +57,6 @@ func (m *ContactMutator) Default(ctx context.Context, obj runtime.Object) error } contactLog.Info("Defaulting Contact", "name", contact.Name) - // Detect if this is an update and SubjectRef changed so that we have to adjust ownerReferences - req, err := admission.RequestFromContext(ctx) - if err != nil { - contactLog.Error(err, "failed to get admission request from context", "name", contact.GetName()) - return errors.NewInternalError(fmt.Errorf("failed to get admission request from context: %w", err)) - } - - if req.Operation == admissionv1.Update && len(req.OldObject.Raw) > 0 { - var oldContact notificationv1alpha1.Contact - if err := json.Unmarshal(req.OldObject.Raw, &oldContact); err != nil { - contactLog.Error(err, "failed to unmarshal old object", "name", contact.GetName()) - return errors.NewInternalError(fmt.Errorf("failed to unmarshal old contact object: %w", err)) - } - if !reflect.DeepEqual(oldContact.Spec.SubjectRef, contact.Spec.SubjectRef) { - // SubjectRef changed: remove all existing owner references. - contact.SetOwnerReferences(nil) - } - } - if contact.Spec.SubjectRef != nil { if contact.Spec.SubjectRef.APIGroup == "iam.miloapis.com" { if contact.Spec.SubjectRef.Kind == "User" { @@ -222,6 +201,11 @@ func (v *ContactValidator) ValidateUpdate(ctx context.Context, oldObj, newObj ru } errs := field.ErrorList{} + // If the SubjectRef changed, reject the update. + if !reflect.DeepEqual(contactNew.Spec.SubjectRef, contactOld.Spec.SubjectRef) { + errs = append(errs, field.Invalid(field.NewPath("spec", "subjectRef"), contactNew.Spec.SubjectRef, "subjectRef is immutable and cannot be updated")) + } + // Validate Email format if contactNew.Spec.Email != contactOld.Spec.Email { if _, err := mail.ParseAddress(contactNew.Spec.Email); err != nil { diff --git a/internal/webhooks/notification/v1alpha1/contact_webhook_test.go b/internal/webhooks/notification/v1alpha1/contact_webhook_test.go index 080f0d18..b33100fa 100644 --- a/internal/webhooks/notification/v1alpha1/contact_webhook_test.go +++ b/internal/webhooks/notification/v1alpha1/contact_webhook_test.go @@ -2,7 +2,6 @@ package v1alpha1 import ( "context" - "encoding/json" "strings" "testing" @@ -10,14 +9,12 @@ import ( iamv1alpha1 "go.miloapis.com/milo/pkg/apis/iam/v1alpha1" notificationv1alpha1 "go.miloapis.com/milo/pkg/apis/notification/v1alpha1" resourcemanagerv1alpha1 "go.miloapis.com/milo/pkg/apis/resourcemanager/v1alpha1" - admissionv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) var runtimeScheme = runtime.NewScheme() @@ -57,15 +54,7 @@ func TestContactMutator_Default(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(runtimeScheme).WithObjects(user).Build() mutator := &ContactMutator{client: fakeClient, scheme: runtimeScheme} - // Build Admission context simulating a CREATE operation - req := admission.Request{ - AdmissionRequest: admissionv1.AdmissionRequest{ - Operation: admissionv1.Create, - }, - } - ctx := admission.NewContextWithRequest(context.Background(), req) - - err := mutator.Default(ctx, contact) + err := mutator.Default(context.Background(), contact) assert.NoError(t, err, "mutator should not return error") // Expect owner reference to be set @@ -78,75 +67,6 @@ func TestContactMutator_Default(t *testing.T) { } } -func TestContactMutator_Update_ChangesOwnerReference(t *testing.T) { - // Prepare old and new Users - oldUser := &iamv1alpha1.User{ - ObjectMeta: metav1.ObjectMeta{ - Name: "old-user", - UID: types.UID("uid-old-user"), - }, - Spec: iamv1alpha1.UserSpec{Email: "old@example.com"}, - } - - newUser := &iamv1alpha1.User{ - ObjectMeta: metav1.ObjectMeta{ - Name: "new-user", - UID: types.UID("uid-new-user"), - }, - Spec: iamv1alpha1.UserSpec{Email: "new@example.com"}, - } - - // Old persisted Contact referencing oldUser - oldContact := ¬ificationv1alpha1.Contact{ - ObjectMeta: metav1.ObjectMeta{Name: "contact-update"}, - Spec: notificationv1alpha1.ContactSpec{ - GivenName: "Update", - FamilyName: "Test", - Email: "update@example.com", - SubjectRef: ¬ificationv1alpha1.SubjectReference{ - APIGroup: "iam.miloapis.com", - Kind: "User", - Name: oldUser.Name, - }, - }, - } - // Simulate existing owner reference pointing to oldUser - oldContact.OwnerReferences = []metav1.OwnerReference{{ - APIVersion: iamv1alpha1.SchemeGroupVersion.String(), - Kind: "User", - Name: oldUser.Name, - UID: oldUser.UID, - }} - - // New object sent in update, subjectRef changed to newUser and still carries old owner reference - newContact := oldContact.DeepCopy() - newContact.Spec.SubjectRef.Name = newUser.Name - // ownerReferences still contains old ref; mutator should replace it - - // Fake client with both users - fakeClient := fake.NewClientBuilder().WithScheme(runtimeScheme).WithObjects(oldUser, newUser).Build() - mutator := &ContactMutator{client: fakeClient, scheme: runtimeScheme} - - // Build admission update context - oldRaw, _ := json.Marshal(oldContact) - req := admission.Request{ - AdmissionRequest: admissionv1.AdmissionRequest{ - Operation: admissionv1.Update, - OldObject: runtime.RawExtension{Raw: oldRaw}, - }, - } - ctx := admission.NewContextWithRequest(context.Background(), req) - - err := mutator.Default(ctx, newContact) - assert.NoError(t, err) - - if assert.Len(t, newContact.OwnerReferences, 1, "should have exactly one owner reference after mutation") { - ref := newContact.OwnerReferences[0] - assert.Equal(t, newUser.Name, ref.Name) - assert.Equal(t, newUser.UID, ref.UID) - } -} - func TestContactValidator_ValidateCreate(t *testing.T) { tests := map[string]struct { contact *notificationv1alpha1.Contact From 269f325dddbf43a88db5531713ceb2d52c374552 Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Tue, 21 Oct 2025 10:20:13 -0300 Subject: [PATCH 10/11] feat: add PlatformAccessApproval and PlatformInvitation CRDs with sample configurations This commit introduces two new Custom Resource Definitions (CRDs): PlatformAccessApproval and PlatformInvitation. The PlatformAccessApproval CRD allows for managing access approvals for users, while the PlatformInvitation CRD facilitates inviting users to the platform. Sample configurations for both CRDs are included, along with necessary updates to the kustomization file to include these new resources. --- ....miloapis.com_platformaccessapprovals.yaml | 157 +++++ .../iam.miloapis.com_platforminvitations.yaml | 178 ++++++ config/crd/bases/iam/kustomization.yaml | 2 + .../iam/v1alpha1/platformaccessapproval.yaml | 14 + .../iam/v1alpha1/platforminvitation.yaml | 15 + docs/api/iam.md | 588 ++++++++++++++++++ .../v1alpha1/platformaccessapproval_types.go | 69 ++ .../iam/v1alpha1/platforminvitation_types.go | 90 +++ pkg/apis/iam/v1alpha1/register.go | 4 + .../iam/v1alpha1/zz_generated.deepcopy.go | 239 +++++++ 10 files changed, 1356 insertions(+) create mode 100644 config/crd/bases/iam/iam.miloapis.com_platformaccessapprovals.yaml create mode 100644 config/crd/bases/iam/iam.miloapis.com_platforminvitations.yaml create mode 100644 config/samples/iam/v1alpha1/platformaccessapproval.yaml create mode 100644 config/samples/iam/v1alpha1/platforminvitation.yaml create mode 100644 pkg/apis/iam/v1alpha1/platformaccessapproval_types.go create mode 100644 pkg/apis/iam/v1alpha1/platforminvitation_types.go diff --git a/config/crd/bases/iam/iam.miloapis.com_platformaccessapprovals.yaml b/config/crd/bases/iam/iam.miloapis.com_platformaccessapprovals.yaml new file mode 100644 index 00000000..1814cbbe --- /dev/null +++ b/config/crd/bases/iam/iam.miloapis.com_platformaccessapprovals.yaml @@ -0,0 +1,157 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: platformaccessapprovals.iam.miloapis.com +spec: + group: iam.miloapis.com + names: + kind: PlatformAccessApproval + listKind: PlatformAccessApprovalList + plural: platformaccessapprovals + singular: platformaccessapproval + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PlatformAccessApproval is the Schema for the platformaccessapprovals API. + It represents a platform access approval for a user. Once the platform access approval is created, an email will be sent to the user. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PlatformAccessApprovalSpec defines the desired state of PlatformAccessApproval. + properties: + approverRef: + description: |- + ApproverRef is the reference to the approver being approved. + If not specified, the approval was made by the system. + properties: + name: + description: Name is the name of the User being referenced. + type: string + required: + - name + type: object + subjectRef: + description: SubjectRef is the reference to the subject being approved. + properties: + email: + description: |- + Email is the email of the user being approved. + Use Email to approve an email address that is not associated with a created user. (e.g. when using PlatformInvitation) + UserRef and Email are mutually exclusive. Exactly one of them must be specified. + type: string + userRef: + description: |- + UserRef is the reference to the user being approved. + UserRef and Email are mutually exclusive. Exactly one of them must be specified. + properties: + name: + description: Name is the name of the User being referenced. + type: string + required: + - name + type: object + type: object + x-kubernetes-validations: + - message: Exactly one of email or userRef must be specified + rule: (has(self.email) && !has(self.userRef)) || (!has(self.email) + && has(self.userRef)) + required: + - subjectRef + type: object + x-kubernetes-validations: + - message: spec is immutable + rule: self == oldSelf + status: + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Platform access approval reconciliation is pending + reason: ReconcilePending + status: Unknown + type: Ready + description: Conditions provide conditions that represent the current + status of the PlatformAccessApproval. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/iam/iam.miloapis.com_platforminvitations.yaml b/config/crd/bases/iam/iam.miloapis.com_platforminvitations.yaml new file mode 100644 index 00000000..bec2b0c0 --- /dev/null +++ b/config/crd/bases/iam/iam.miloapis.com_platforminvitations.yaml @@ -0,0 +1,178 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: platforminvitations.iam.miloapis.com +spec: + group: iam.miloapis.com + names: + kind: PlatformInvitation + listKind: PlatformInvitationList + plural: platforminvitations + singular: platforminvitation + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.email + name: Email + type: string + - jsonPath: .spec.scheduleAt + name: Schedule At + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + PlatformInvitation is the Schema for the platforminvitations API + It represents a platform invitation for a user. Once the platform invitation is created, an email will be sent to the user to invite them to the platform. + The invited user will have access to the platform after they create an account using the asociated email. + It represents a platform invitation for a user. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: PlatformInvitationSpec defines the desired state of PlatformInvitation. + properties: + email: + description: The email of the user being invited. + type: string + x-kubernetes-validations: + - message: email type is immutable + rule: type(oldSelf) == null_type || self == oldSelf + familyName: + description: The family name of the user being invited. + type: string + givenName: + description: The given name of the user being invited. + type: string + invitedBy: + description: The user who created the platform invitation. A mutation + webhook will default this field to the user who made the request. + properties: + name: + description: Name is the name of the User being referenced. + type: string + required: + - name + type: object + x-kubernetes-validations: + - message: invitedBy type is immutable + rule: type(oldSelf) == null_type || self == oldSelf + scheduleAt: + description: |- + The schedule at which the platform invitation will be sent. + It can only be updated before the platform invitation is sent. + format: date-time + type: string + required: + - email + type: object + status: + description: PlatformInvitationStatus defines the observed state of PlatformInvitation. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Platform invitation reconciliation is pending + reason: ReconcilePending + status: Unknown + type: Ready + description: Conditions provide conditions that represent the current + status of the PlatformInvitation. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + email: + description: The email resource that was created for the platform + invitation. + properties: + name: + description: The name of the email resource that was created for + the platform invitation. + type: string + namespace: + description: The namespace of the email resource that was created + for the platform invitation. + type: string + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/iam/kustomization.yaml b/config/crd/bases/iam/kustomization.yaml index 2c142907..797c8a51 100644 --- a/config/crd/bases/iam/kustomization.yaml +++ b/config/crd/bases/iam/kustomization.yaml @@ -10,3 +10,5 @@ resources: - iam.miloapis.com_machineaccountkeys.yaml - iam.miloapis.com_userpreferences.yaml - iam.miloapis.com_userdeactivations.yaml +- iam.miloapis.com_platforminvitations.yaml +- iam.miloapis.com_platformaccessapprovals.yaml diff --git a/config/samples/iam/v1alpha1/platformaccessapproval.yaml b/config/samples/iam/v1alpha1/platformaccessapproval.yaml new file mode 100644 index 00000000..e2676e36 --- /dev/null +++ b/config/samples/iam/v1alpha1/platformaccessapproval.yaml @@ -0,0 +1,14 @@ +apiVersion: iam.miloapis.com/v1alpha1 +kind: PlatformAccessApproval +metadata: + name: platformaccessapproval-sample +spec: + # Subject being approved – provide either email or userRef + subjectRef: + email: invited.user@example.com + # Alternatively, reference an existing user: + # userRef: + # name: some-user + # Approver of this access request (optional) + approverRef: + name: admin-user diff --git a/config/samples/iam/v1alpha1/platforminvitation.yaml b/config/samples/iam/v1alpha1/platforminvitation.yaml new file mode 100644 index 00000000..2fa31fda --- /dev/null +++ b/config/samples/iam/v1alpha1/platforminvitation.yaml @@ -0,0 +1,15 @@ +apiVersion: iam.miloapis.com/v1alpha1 +kind: PlatformInvitation +metadata: + name: platforminvitation-sample +spec: + # The email address to invite to the platform + email: invited.user@example.com + # Optional personal details for the invited user + givenName: Invited + familyName: User + # (Optional) Schedule when the invitation email should be sent + # scheduleAt: "2025-12-25T12:00:00Z" + # Identify who created the invitation + invitedBy: + name: admin-user diff --git a/docs/api/iam.md b/docs/api/iam.md index 435b8207..ecd6d9c4 100644 --- a/docs/api/iam.md +++ b/docs/api/iam.md @@ -16,6 +16,10 @@ Resource Types: - [MachineAccount](#machineaccount) +- [PlatformAccessApproval](#platformaccessapproval) + +- [PlatformInvitation](#platforminvitation) + - [PolicyBinding](#policybinding) - [ProtectedResource](#protectedresource) @@ -862,6 +866,590 @@ with respect to the current state of the instance.
+## PlatformAccessApproval +[↩ Parent](#iammiloapiscomv1alpha1 ) + + + + + + +PlatformAccessApproval is the Schema for the platformaccessapprovals API. +It represents a platform access approval for a user. Once the platform access approval is created, an email will be sent to the user. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionRequired
apiVersionstringiam.miloapis.com/v1alpha1true
kindstringPlatformAccessApprovaltrue
metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
specobject + PlatformAccessApprovalSpec defines the desired state of PlatformAccessApproval.
+
+ Validations:
  • self == oldSelf: spec is immutable
  • +
    false
    statusobject +
    +
    false
    + + +### PlatformAccessApproval.spec +[↩ Parent](#platformaccessapproval) + + + +PlatformAccessApprovalSpec defines the desired state of PlatformAccessApproval. + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    subjectRefobject + SubjectRef is the reference to the subject being approved.
    +
    + Validations:
  • (has(self.email) && !has(self.userRef)) || (!has(self.email) && has(self.userRef)): Exactly one of email or userRef must be specified
  • +
    true
    approverRefobject + ApproverRef is the reference to the approver being approved. +If not specified, the approval was made by the system.
    +
    false
    + + +### PlatformAccessApproval.spec.subjectRef +[↩ Parent](#platformaccessapprovalspec) + + + +SubjectRef is the reference to the subject being approved. + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    emailstring + Email is the email of the user being approved. +Use Email to approve an email address that is not associated with a created user. (e.g. when using PlatformInvitation) +UserRef and Email are mutually exclusive. Exactly one of them must be specified.
    +
    false
    userRefobject + UserRef is the reference to the user being approved. +UserRef and Email are mutually exclusive. Exactly one of them must be specified.
    +
    false
    + + +### PlatformAccessApproval.spec.subjectRef.userRef +[↩ Parent](#platformaccessapprovalspecsubjectref) + + + +UserRef is the reference to the user being approved. +UserRef and Email are mutually exclusive. Exactly one of them must be specified. + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring + Name is the name of the User being referenced.
    +
    true
    + + +### PlatformAccessApproval.spec.approverRef +[↩ Parent](#platformaccessapprovalspec) + + + +ApproverRef is the reference to the approver being approved. +If not specified, the approval was made by the system. + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring + Name is the name of the User being referenced.
    +
    true
    + + +### PlatformAccessApproval.status +[↩ Parent](#platformaccessapproval) + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    conditions[]object + Conditions provide conditions that represent the current status of the PlatformAccessApproval.
    +
    + Default: [map[lastTransitionTime:1970-01-01T00:00:00Z message:Platform access approval reconciliation is pending reason:ReconcilePending status:Unknown type:Ready]]
    +
    false
    + + +### PlatformAccessApproval.status.conditions[index] +[↩ Parent](#platformaccessapprovalstatus) + + + +Condition contains details for one aspect of the current state of this API Resource. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    lastTransitionTimestring + lastTransitionTime is the last time the condition transitioned from one status to another. +This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
    +
    + Format: date-time
    +
    true
    messagestring + message is a human readable message indicating details about the transition. +This may be an empty string.
    +
    true
    reasonstring + reason contains a programmatic identifier indicating the reason for the condition's last transition. +Producers of specific condition types may define expected values and meanings for this field, +and whether the values are considered a guaranteed API. +The value should be a CamelCase string. +This field may not be empty.
    +
    true
    statusenum + status of the condition, one of True, False, Unknown.
    +
    + Enum: True, False, Unknown
    +
    true
    typestring + type of condition in CamelCase or in foo.example.com/CamelCase.
    +
    true
    observedGenerationinteger + observedGeneration represents the .metadata.generation that the condition was set based upon. +For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date +with respect to the current state of the instance.
    +
    + Format: int64
    + Minimum: 0
    +
    false
    + +## PlatformInvitation +[↩ Parent](#iammiloapiscomv1alpha1 ) + + + + + + +PlatformInvitation is the Schema for the platforminvitations API +It represents a platform invitation for a user. Once the platform invitation is created, an email will be sent to the user to invite them to the platform. +The invited user will have access to the platform after they create an account using the asociated email. +It represents a platform invitation for a user. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    apiVersionstringiam.miloapis.com/v1alpha1true
    kindstringPlatformInvitationtrue
    metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
    specobject + PlatformInvitationSpec defines the desired state of PlatformInvitation.
    +
    false
    statusobject + PlatformInvitationStatus defines the observed state of PlatformInvitation.
    +
    false
    + + +### PlatformInvitation.spec +[↩ Parent](#platforminvitation) + + + +PlatformInvitationSpec defines the desired state of PlatformInvitation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    emailstring + The email of the user being invited.
    +
    + Validations:
  • type(oldSelf) == null_type || self == oldSelf: email type is immutable
  • +
    true
    familyNamestring + The family name of the user being invited.
    +
    false
    givenNamestring + The given name of the user being invited.
    +
    false
    invitedByobject + The user who created the platform invitation. A mutation webhook will default this field to the user who made the request.
    +
    + Validations:
  • type(oldSelf) == null_type || self == oldSelf: invitedBy type is immutable
  • +
    false
    scheduleAtstring + The schedule at which the platform invitation will be sent. +It can only be updated before the platform invitation is sent.
    +
    + Format: date-time
    +
    false
    + + +### PlatformInvitation.spec.invitedBy +[↩ Parent](#platforminvitationspec) + + + +The user who created the platform invitation. A mutation webhook will default this field to the user who made the request. + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring + Name is the name of the User being referenced.
    +
    true
    + + +### PlatformInvitation.status +[↩ Parent](#platforminvitation) + + + +PlatformInvitationStatus defines the observed state of PlatformInvitation. + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    conditions[]object + Conditions provide conditions that represent the current status of the PlatformInvitation.
    +
    + Default: [map[lastTransitionTime:1970-01-01T00:00:00Z message:Platform invitation reconciliation is pending reason:ReconcilePending status:Unknown type:Ready]]
    +
    false
    emailobject + The email resource that was created for the platform invitation.
    +
    false
    + + +### PlatformInvitation.status.conditions[index] +[↩ Parent](#platforminvitationstatus) + + + +Condition contains details for one aspect of the current state of this API Resource. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    lastTransitionTimestring + lastTransitionTime is the last time the condition transitioned from one status to another. +This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
    +
    + Format: date-time
    +
    true
    messagestring + message is a human readable message indicating details about the transition. +This may be an empty string.
    +
    true
    reasonstring + reason contains a programmatic identifier indicating the reason for the condition's last transition. +Producers of specific condition types may define expected values and meanings for this field, +and whether the values are considered a guaranteed API. +The value should be a CamelCase string. +This field may not be empty.
    +
    true
    statusenum + status of the condition, one of True, False, Unknown.
    +
    + Enum: True, False, Unknown
    +
    true
    typestring + type of condition in CamelCase or in foo.example.com/CamelCase.
    +
    true
    observedGenerationinteger + observedGeneration represents the .metadata.generation that the condition was set based upon. +For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date +with respect to the current state of the instance.
    +
    + Format: int64
    + Minimum: 0
    +
    false
    + + +### PlatformInvitation.status.email +[↩ Parent](#platforminvitationstatus) + + + +The email resource that was created for the platform invitation. + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescriptionRequired
    namestring + The name of the email resource that was created for the platform invitation.
    +
    false
    namespacestring + The namespace of the email resource that was created for the platform invitation.
    +
    false
    + ## PolicyBinding [↩ Parent](#iammiloapiscomv1alpha1 ) diff --git a/pkg/apis/iam/v1alpha1/platformaccessapproval_types.go b/pkg/apis/iam/v1alpha1/platformaccessapproval_types.go new file mode 100644 index 00000000..984a7956 --- /dev/null +++ b/pkg/apis/iam/v1alpha1/platformaccessapproval_types.go @@ -0,0 +1,69 @@ +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// Create conditions +const ( + // PlatformAccessApprovalReadyCondition is the condition Type that tracks platform access approval creation status. + PlatformAccessApprovalReadyCondition = "Ready" + // PlatformAccessApprovalReconciledReason is used when platform access approval reconciliation succeeds. + PlatformAccessApprovalReconciledReason = "Reconciled" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// PlatformAccessApproval is the Schema for the platformaccessapprovals API. +// It represents a platform access approval for a user. Once the platform access approval is created, an email will be sent to the user. +// +kubebuilder:resource:scope=Cluster +type PlatformAccessApproval struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PlatformAccessApprovalSpec `json:"spec,omitempty"` + Status PlatformAccessApprovalStatus `json:"status,omitempty"` +} + +// PlatformAccessApprovalSpec defines the desired state of PlatformAccessApproval. +// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec is immutable" +// +kubebuilder:validation:Type=object +type PlatformAccessApprovalSpec struct { + // SubjectRef is the reference to the subject being approved. + // +kubebuilder:validation:Required + SubjectRef SubjectReference `json:"subjectRef"` + // ApproverRef is the reference to the approver being approved. + // If not specified, the approval was made by the system. + // +kubebuilder:validation:Optional + ApproverRef *UserReference `json:"approverRef,omitempty"` +} + +// +kubebuilder:object:root=true + +// PlatformAccessApprovalList contains a list of PlatformAccessApproval. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type PlatformAccessApprovalList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PlatformAccessApproval `json:"items"` +} + +type PlatformAccessApprovalStatus struct { + // Conditions provide conditions that represent the current status of the PlatformAccessApproval. + // +kubebuilder:default={{type: "Ready", status: "Unknown", reason: "ReconcilePending", message: "Platform access approval reconciliation is pending", lastTransitionTime: "1970-01-01T00:00:00Z"}} + // +kubebuilder:validation:Optional + Conditions []metav1.Condition `json:"conditions,omitempty"` +} + +// +kubebuilder:validation:XValidation:rule="(has(self.email) && !has(self.userRef)) || (!has(self.email) && has(self.userRef))",message="Exactly one of email or userRef must be specified" +type SubjectReference struct { + // Email is the email of the user being approved. + // Use Email to approve an email address that is not associated with a created user. (e.g. when using PlatformInvitation) + // UserRef and Email are mutually exclusive. Exactly one of them must be specified. + // +kubebuilder:validation:Optional + Email string `json:"email,omitempty"` + // UserRef is the reference to the user being approved. + // UserRef and Email are mutually exclusive. Exactly one of them must be specified. + // +kubebuilder:validation:Optional + UserRef *UserReference `json:"userRef,omitempty"` +} diff --git a/pkg/apis/iam/v1alpha1/platforminvitation_types.go b/pkg/apis/iam/v1alpha1/platforminvitation_types.go new file mode 100644 index 00000000..23d8e7a1 --- /dev/null +++ b/pkg/apis/iam/v1alpha1/platforminvitation_types.go @@ -0,0 +1,90 @@ +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// Create conditions +const ( + // PlatformInvitationReadyCondition is the condition Type that tracks platform invitation creation status. + PlatformInvitationReadyCondition = "Ready" + // PlatformInvitationReconciledReason is used when platform invitation reconciliation succeeds. + PlatformInvitationReconciledReason = "Reconciled" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// PlatformInvitation is the Schema for the platforminvitations API +// It represents a platform invitation for a user. Once the platform invitation is created, an email will be sent to the user to invite them to the platform. +// The invited user will have access to the platform after they create an account using the asociated email. +// +kubebuilder:printcolumn:name="Email",type=string,JSONPath=".spec.email" +// +kubebuilder:printcolumn:name="Schedule At",type="string",JSONPath=".spec.scheduleAt" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" +// It represents a platform invitation for a user. +// +kubebuilder:resource:scope=Cluster +type PlatformInvitation struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec PlatformInvitationSpec `json:"spec,omitempty"` + Status PlatformInvitationStatus `json:"status,omitempty"` +} + +// PlatformInvitationSpec defines the desired state of PlatformInvitation. +// +kubebuilder:validation:Type=object +type PlatformInvitationSpec struct { + // The email of the user being invited. + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="type(oldSelf) == null_type || self == oldSelf",message="email type is immutable" + Email string `json:"email"` + + // The given name of the user being invited. + // +kubebuilder:validation:Optional + GivenName string `json:"givenName,omitempty"` + + // The family name of the user being invited. + // +kubebuilder:validation:Optional + FamilyName string `json:"familyName,omitempty"` + + // The schedule at which the platform invitation will be sent. + // It can only be updated before the platform invitation is sent. + // +kubebuilder:validation:Optional + ScheduleAt *metav1.Time `json:"scheduleAt,omitempty"` + + // The user who created the platform invitation. A mutation webhook will default this field to the user who made the request. + // +kubebuilder:validation:XValidation:rule="type(oldSelf) == null_type || self == oldSelf",message="invitedBy type is immutable" + InvitedBy UserReference `json:"invitedBy,omitempty"` +} + +// +kubebuilder:object:root=true + +// PlatformInvitationList contains a list of PlatformInvitation. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type PlatformInvitationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PlatformInvitation `json:"items"` +} + +// PlatformInvitationStatus defines the observed state of PlatformInvitation. +// +kubebuilder:validation:Type=object +type PlatformInvitationStatus struct { + // Conditions provide conditions that represent the current status of the PlatformInvitation. + // +kubebuilder:default={{type: "Ready", status: "Unknown", reason: "ReconcilePending", message: "Platform invitation reconciliation is pending", lastTransitionTime: "1970-01-01T00:00:00Z"}} + // +kubebuilder:validation:Optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // The email resource that was created for the platform invitation. + // +kubebuilder:validation:Optional + Email PlatformInvitationEmailStatus `json:"email,omitempty"` +} + +type PlatformInvitationEmailStatus struct { + // The name of the email resource that was created for the platform invitation. + // +kubebuilder:validation:Optional + Name string `json:"name,omitempty"` + // The namespace of the email resource that was created for the platform invitation. + // +kubebuilder:validation:Optional + Namespace string `json:"namespace,omitempty"` +} diff --git a/pkg/apis/iam/v1alpha1/register.go b/pkg/apis/iam/v1alpha1/register.go index 35cae7ad..20b75502 100644 --- a/pkg/apis/iam/v1alpha1/register.go +++ b/pkg/apis/iam/v1alpha1/register.go @@ -43,6 +43,10 @@ func addKnownTypes(scheme *runtime.Scheme) error { &UserDeactivationList{}, &UserInvitation{}, &UserInvitationList{}, + &PlatformInvitation{}, + &PlatformInvitationList{}, + &PlatformAccessApproval{}, + &PlatformAccessApprovalList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/iam/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iam/v1alpha1/zz_generated.deepcopy.go index e5b93a4d..0d44a181 100644 --- a/pkg/apis/iam/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/iam/v1alpha1/zz_generated.deepcopy.go @@ -413,6 +413,225 @@ func (in *ParentResourceRef) DeepCopy() *ParentResourceRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAccessApproval) DeepCopyInto(out *PlatformAccessApproval) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAccessApproval. +func (in *PlatformAccessApproval) DeepCopy() *PlatformAccessApproval { + if in == nil { + return nil + } + out := new(PlatformAccessApproval) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlatformAccessApproval) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAccessApprovalList) DeepCopyInto(out *PlatformAccessApprovalList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PlatformAccessApproval, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAccessApprovalList. +func (in *PlatformAccessApprovalList) DeepCopy() *PlatformAccessApprovalList { + if in == nil { + return nil + } + out := new(PlatformAccessApprovalList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlatformAccessApprovalList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAccessApprovalSpec) DeepCopyInto(out *PlatformAccessApprovalSpec) { + *out = *in + in.SubjectRef.DeepCopyInto(&out.SubjectRef) + if in.ApproverRef != nil { + in, out := &in.ApproverRef, &out.ApproverRef + *out = new(UserReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAccessApprovalSpec. +func (in *PlatformAccessApprovalSpec) DeepCopy() *PlatformAccessApprovalSpec { + if in == nil { + return nil + } + out := new(PlatformAccessApprovalSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformAccessApprovalStatus) DeepCopyInto(out *PlatformAccessApprovalStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAccessApprovalStatus. +func (in *PlatformAccessApprovalStatus) DeepCopy() *PlatformAccessApprovalStatus { + if in == nil { + return nil + } + out := new(PlatformAccessApprovalStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformInvitation) DeepCopyInto(out *PlatformInvitation) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitation. +func (in *PlatformInvitation) DeepCopy() *PlatformInvitation { + if in == nil { + return nil + } + out := new(PlatformInvitation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlatformInvitation) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformInvitationEmailStatus) DeepCopyInto(out *PlatformInvitationEmailStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitationEmailStatus. +func (in *PlatformInvitationEmailStatus) DeepCopy() *PlatformInvitationEmailStatus { + if in == nil { + return nil + } + out := new(PlatformInvitationEmailStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformInvitationList) DeepCopyInto(out *PlatformInvitationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PlatformInvitation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitationList. +func (in *PlatformInvitationList) DeepCopy() *PlatformInvitationList { + if in == nil { + return nil + } + out := new(PlatformInvitationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PlatformInvitationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformInvitationSpec) DeepCopyInto(out *PlatformInvitationSpec) { + *out = *in + if in.ScheduleAt != nil { + in, out := &in.ScheduleAt, &out.ScheduleAt + *out = (*in).DeepCopy() + } + out.InvitedBy = in.InvitedBy +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitationSpec. +func (in *PlatformInvitationSpec) DeepCopy() *PlatformInvitationSpec { + if in == nil { + return nil + } + out := new(PlatformInvitationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PlatformInvitationStatus) DeepCopyInto(out *PlatformInvitationStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.Email = in.Email +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitationStatus. +func (in *PlatformInvitationStatus) DeepCopy() *PlatformInvitationStatus { + if in == nil { + return nil + } + out := new(PlatformInvitationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PolicyBinding) DeepCopyInto(out *PolicyBinding) { *out = *in @@ -844,6 +1063,26 @@ func (in *Subject) DeepCopy() *Subject { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SubjectReference) DeepCopyInto(out *SubjectReference) { + *out = *in + if in.UserRef != nil { + in, out := &in.UserRef, &out.UserRef + *out = new(UserReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubjectReference. +func (in *SubjectReference) DeepCopy() *SubjectReference { + if in == nil { + return nil + } + out := new(SubjectReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *User) DeepCopyInto(out *User) { *out = *in From 3225f256dc7d9be7b4d0acc1135f53d05dbe3da2 Mon Sep 17 00:00:00 2001 From: Jose Szychowski Date: Tue, 21 Oct 2025 17:15:00 -0300 Subject: [PATCH 11/11] Revert commit in wrong branch "feat: add PlatformAccessApproval and PlatformInvitation CRDs with sample configurations" This reverts commit 269f325dddbf43a88db5531713ceb2d52c374552. --- ....miloapis.com_platformaccessapprovals.yaml | 157 ----- .../iam.miloapis.com_platforminvitations.yaml | 178 ------ config/crd/bases/iam/kustomization.yaml | 2 - .../iam/v1alpha1/platformaccessapproval.yaml | 14 - .../iam/v1alpha1/platforminvitation.yaml | 15 - docs/api/iam.md | 588 ------------------ .../v1alpha1/platformaccessapproval_types.go | 69 -- .../iam/v1alpha1/platforminvitation_types.go | 90 --- pkg/apis/iam/v1alpha1/register.go | 4 - .../iam/v1alpha1/zz_generated.deepcopy.go | 239 ------- 10 files changed, 1356 deletions(-) delete mode 100644 config/crd/bases/iam/iam.miloapis.com_platformaccessapprovals.yaml delete mode 100644 config/crd/bases/iam/iam.miloapis.com_platforminvitations.yaml delete mode 100644 config/samples/iam/v1alpha1/platformaccessapproval.yaml delete mode 100644 config/samples/iam/v1alpha1/platforminvitation.yaml delete mode 100644 pkg/apis/iam/v1alpha1/platformaccessapproval_types.go delete mode 100644 pkg/apis/iam/v1alpha1/platforminvitation_types.go diff --git a/config/crd/bases/iam/iam.miloapis.com_platformaccessapprovals.yaml b/config/crd/bases/iam/iam.miloapis.com_platformaccessapprovals.yaml deleted file mode 100644 index 1814cbbe..00000000 --- a/config/crd/bases/iam/iam.miloapis.com_platformaccessapprovals.yaml +++ /dev/null @@ -1,157 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.18.0 - name: platformaccessapprovals.iam.miloapis.com -spec: - group: iam.miloapis.com - names: - kind: PlatformAccessApproval - listKind: PlatformAccessApprovalList - plural: platformaccessapprovals - singular: platformaccessapproval - scope: Cluster - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: |- - PlatformAccessApproval is the Schema for the platformaccessapprovals API. - It represents a platform access approval for a user. Once the platform access approval is created, an email will be sent to the user. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: PlatformAccessApprovalSpec defines the desired state of PlatformAccessApproval. - properties: - approverRef: - description: |- - ApproverRef is the reference to the approver being approved. - If not specified, the approval was made by the system. - properties: - name: - description: Name is the name of the User being referenced. - type: string - required: - - name - type: object - subjectRef: - description: SubjectRef is the reference to the subject being approved. - properties: - email: - description: |- - Email is the email of the user being approved. - Use Email to approve an email address that is not associated with a created user. (e.g. when using PlatformInvitation) - UserRef and Email are mutually exclusive. Exactly one of them must be specified. - type: string - userRef: - description: |- - UserRef is the reference to the user being approved. - UserRef and Email are mutually exclusive. Exactly one of them must be specified. - properties: - name: - description: Name is the name of the User being referenced. - type: string - required: - - name - type: object - type: object - x-kubernetes-validations: - - message: Exactly one of email or userRef must be specified - rule: (has(self.email) && !has(self.userRef)) || (!has(self.email) - && has(self.userRef)) - required: - - subjectRef - type: object - x-kubernetes-validations: - - message: spec is immutable - rule: self == oldSelf - status: - properties: - conditions: - default: - - lastTransitionTime: "1970-01-01T00:00:00Z" - message: Platform access approval reconciliation is pending - reason: ReconcilePending - status: Unknown - type: Ready - description: Conditions provide conditions that represent the current - status of the PlatformAccessApproval. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/iam/iam.miloapis.com_platforminvitations.yaml b/config/crd/bases/iam/iam.miloapis.com_platforminvitations.yaml deleted file mode 100644 index bec2b0c0..00000000 --- a/config/crd/bases/iam/iam.miloapis.com_platforminvitations.yaml +++ /dev/null @@ -1,178 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.18.0 - name: platforminvitations.iam.miloapis.com -spec: - group: iam.miloapis.com - names: - kind: PlatformInvitation - listKind: PlatformInvitationList - plural: platforminvitations - singular: platforminvitation - scope: Cluster - versions: - - additionalPrinterColumns: - - jsonPath: .spec.email - name: Email - type: string - - jsonPath: .spec.scheduleAt - name: Schedule At - type: string - - jsonPath: .status.conditions[?(@.type=='Ready')].status - name: Ready - type: string - - jsonPath: .metadata.creationTimestamp - name: Age - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: |- - PlatformInvitation is the Schema for the platforminvitations API - It represents a platform invitation for a user. Once the platform invitation is created, an email will be sent to the user to invite them to the platform. - The invited user will have access to the platform after they create an account using the asociated email. - It represents a platform invitation for a user. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: PlatformInvitationSpec defines the desired state of PlatformInvitation. - properties: - email: - description: The email of the user being invited. - type: string - x-kubernetes-validations: - - message: email type is immutable - rule: type(oldSelf) == null_type || self == oldSelf - familyName: - description: The family name of the user being invited. - type: string - givenName: - description: The given name of the user being invited. - type: string - invitedBy: - description: The user who created the platform invitation. A mutation - webhook will default this field to the user who made the request. - properties: - name: - description: Name is the name of the User being referenced. - type: string - required: - - name - type: object - x-kubernetes-validations: - - message: invitedBy type is immutable - rule: type(oldSelf) == null_type || self == oldSelf - scheduleAt: - description: |- - The schedule at which the platform invitation will be sent. - It can only be updated before the platform invitation is sent. - format: date-time - type: string - required: - - email - type: object - status: - description: PlatformInvitationStatus defines the observed state of PlatformInvitation. - properties: - conditions: - default: - - lastTransitionTime: "1970-01-01T00:00:00Z" - message: Platform invitation reconciliation is pending - reason: ReconcilePending - status: Unknown - type: Ready - description: Conditions provide conditions that represent the current - status of the PlatformInvitation. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - email: - description: The email resource that was created for the platform - invitation. - properties: - name: - description: The name of the email resource that was created for - the platform invitation. - type: string - namespace: - description: The namespace of the email resource that was created - for the platform invitation. - type: string - type: object - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/iam/kustomization.yaml b/config/crd/bases/iam/kustomization.yaml index 797c8a51..2c142907 100644 --- a/config/crd/bases/iam/kustomization.yaml +++ b/config/crd/bases/iam/kustomization.yaml @@ -10,5 +10,3 @@ resources: - iam.miloapis.com_machineaccountkeys.yaml - iam.miloapis.com_userpreferences.yaml - iam.miloapis.com_userdeactivations.yaml -- iam.miloapis.com_platforminvitations.yaml -- iam.miloapis.com_platformaccessapprovals.yaml diff --git a/config/samples/iam/v1alpha1/platformaccessapproval.yaml b/config/samples/iam/v1alpha1/platformaccessapproval.yaml deleted file mode 100644 index e2676e36..00000000 --- a/config/samples/iam/v1alpha1/platformaccessapproval.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: iam.miloapis.com/v1alpha1 -kind: PlatformAccessApproval -metadata: - name: platformaccessapproval-sample -spec: - # Subject being approved – provide either email or userRef - subjectRef: - email: invited.user@example.com - # Alternatively, reference an existing user: - # userRef: - # name: some-user - # Approver of this access request (optional) - approverRef: - name: admin-user diff --git a/config/samples/iam/v1alpha1/platforminvitation.yaml b/config/samples/iam/v1alpha1/platforminvitation.yaml deleted file mode 100644 index 2fa31fda..00000000 --- a/config/samples/iam/v1alpha1/platforminvitation.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: iam.miloapis.com/v1alpha1 -kind: PlatformInvitation -metadata: - name: platforminvitation-sample -spec: - # The email address to invite to the platform - email: invited.user@example.com - # Optional personal details for the invited user - givenName: Invited - familyName: User - # (Optional) Schedule when the invitation email should be sent - # scheduleAt: "2025-12-25T12:00:00Z" - # Identify who created the invitation - invitedBy: - name: admin-user diff --git a/docs/api/iam.md b/docs/api/iam.md index ecd6d9c4..435b8207 100644 --- a/docs/api/iam.md +++ b/docs/api/iam.md @@ -16,10 +16,6 @@ Resource Types: - [MachineAccount](#machineaccount) -- [PlatformAccessApproval](#platformaccessapproval) - -- [PlatformInvitation](#platforminvitation) - - [PolicyBinding](#policybinding) - [ProtectedResource](#protectedresource) @@ -866,590 +862,6 @@ with respect to the current state of the instance.
    -## PlatformAccessApproval -[↩ Parent](#iammiloapiscomv1alpha1 ) - - - - - - -PlatformAccessApproval is the Schema for the platformaccessapprovals API. -It represents a platform access approval for a user. Once the platform access approval is created, an email will be sent to the user. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    apiVersionstringiam.miloapis.com/v1alpha1true
    kindstringPlatformAccessApprovaltrue
    metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
    specobject - PlatformAccessApprovalSpec defines the desired state of PlatformAccessApproval.
    -
    - Validations:
  • self == oldSelf: spec is immutable
  • -
    false
    statusobject -
    -
    false
    - - -### PlatformAccessApproval.spec -[↩ Parent](#platformaccessapproval) - - - -PlatformAccessApprovalSpec defines the desired state of PlatformAccessApproval. - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    subjectRefobject - SubjectRef is the reference to the subject being approved.
    -
    - Validations:
  • (has(self.email) && !has(self.userRef)) || (!has(self.email) && has(self.userRef)): Exactly one of email or userRef must be specified
  • -
    true
    approverRefobject - ApproverRef is the reference to the approver being approved. -If not specified, the approval was made by the system.
    -
    false
    - - -### PlatformAccessApproval.spec.subjectRef -[↩ Parent](#platformaccessapprovalspec) - - - -SubjectRef is the reference to the subject being approved. - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    emailstring - Email is the email of the user being approved. -Use Email to approve an email address that is not associated with a created user. (e.g. when using PlatformInvitation) -UserRef and Email are mutually exclusive. Exactly one of them must be specified.
    -
    false
    userRefobject - UserRef is the reference to the user being approved. -UserRef and Email are mutually exclusive. Exactly one of them must be specified.
    -
    false
    - - -### PlatformAccessApproval.spec.subjectRef.userRef -[↩ Parent](#platformaccessapprovalspecsubjectref) - - - -UserRef is the reference to the user being approved. -UserRef and Email are mutually exclusive. Exactly one of them must be specified. - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    namestring - Name is the name of the User being referenced.
    -
    true
    - - -### PlatformAccessApproval.spec.approverRef -[↩ Parent](#platformaccessapprovalspec) - - - -ApproverRef is the reference to the approver being approved. -If not specified, the approval was made by the system. - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    namestring - Name is the name of the User being referenced.
    -
    true
    - - -### PlatformAccessApproval.status -[↩ Parent](#platformaccessapproval) - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    conditions[]object - Conditions provide conditions that represent the current status of the PlatformAccessApproval.
    -
    - Default: [map[lastTransitionTime:1970-01-01T00:00:00Z message:Platform access approval reconciliation is pending reason:ReconcilePending status:Unknown type:Ready]]
    -
    false
    - - -### PlatformAccessApproval.status.conditions[index] -[↩ Parent](#platformaccessapprovalstatus) - - - -Condition contains details for one aspect of the current state of this API Resource. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    lastTransitionTimestring - lastTransitionTime is the last time the condition transitioned from one status to another. -This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
    -
    - Format: date-time
    -
    true
    messagestring - message is a human readable message indicating details about the transition. -This may be an empty string.
    -
    true
    reasonstring - reason contains a programmatic identifier indicating the reason for the condition's last transition. -Producers of specific condition types may define expected values and meanings for this field, -and whether the values are considered a guaranteed API. -The value should be a CamelCase string. -This field may not be empty.
    -
    true
    statusenum - status of the condition, one of True, False, Unknown.
    -
    - Enum: True, False, Unknown
    -
    true
    typestring - type of condition in CamelCase or in foo.example.com/CamelCase.
    -
    true
    observedGenerationinteger - observedGeneration represents the .metadata.generation that the condition was set based upon. -For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date -with respect to the current state of the instance.
    -
    - Format: int64
    - Minimum: 0
    -
    false
    - -## PlatformInvitation -[↩ Parent](#iammiloapiscomv1alpha1 ) - - - - - - -PlatformInvitation is the Schema for the platforminvitations API -It represents a platform invitation for a user. Once the platform invitation is created, an email will be sent to the user to invite them to the platform. -The invited user will have access to the platform after they create an account using the asociated email. -It represents a platform invitation for a user. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    apiVersionstringiam.miloapis.com/v1alpha1true
    kindstringPlatformInvitationtrue
    metadataobjectRefer to the Kubernetes API documentation for the fields of the `metadata` field.true
    specobject - PlatformInvitationSpec defines the desired state of PlatformInvitation.
    -
    false
    statusobject - PlatformInvitationStatus defines the observed state of PlatformInvitation.
    -
    false
    - - -### PlatformInvitation.spec -[↩ Parent](#platforminvitation) - - - -PlatformInvitationSpec defines the desired state of PlatformInvitation. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    emailstring - The email of the user being invited.
    -
    - Validations:
  • type(oldSelf) == null_type || self == oldSelf: email type is immutable
  • -
    true
    familyNamestring - The family name of the user being invited.
    -
    false
    givenNamestring - The given name of the user being invited.
    -
    false
    invitedByobject - The user who created the platform invitation. A mutation webhook will default this field to the user who made the request.
    -
    - Validations:
  • type(oldSelf) == null_type || self == oldSelf: invitedBy type is immutable
  • -
    false
    scheduleAtstring - The schedule at which the platform invitation will be sent. -It can only be updated before the platform invitation is sent.
    -
    - Format: date-time
    -
    false
    - - -### PlatformInvitation.spec.invitedBy -[↩ Parent](#platforminvitationspec) - - - -The user who created the platform invitation. A mutation webhook will default this field to the user who made the request. - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    namestring - Name is the name of the User being referenced.
    -
    true
    - - -### PlatformInvitation.status -[↩ Parent](#platforminvitation) - - - -PlatformInvitationStatus defines the observed state of PlatformInvitation. - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    conditions[]object - Conditions provide conditions that represent the current status of the PlatformInvitation.
    -
    - Default: [map[lastTransitionTime:1970-01-01T00:00:00Z message:Platform invitation reconciliation is pending reason:ReconcilePending status:Unknown type:Ready]]
    -
    false
    emailobject - The email resource that was created for the platform invitation.
    -
    false
    - - -### PlatformInvitation.status.conditions[index] -[↩ Parent](#platforminvitationstatus) - - - -Condition contains details for one aspect of the current state of this API Resource. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    lastTransitionTimestring - lastTransitionTime is the last time the condition transitioned from one status to another. -This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
    -
    - Format: date-time
    -
    true
    messagestring - message is a human readable message indicating details about the transition. -This may be an empty string.
    -
    true
    reasonstring - reason contains a programmatic identifier indicating the reason for the condition's last transition. -Producers of specific condition types may define expected values and meanings for this field, -and whether the values are considered a guaranteed API. -The value should be a CamelCase string. -This field may not be empty.
    -
    true
    statusenum - status of the condition, one of True, False, Unknown.
    -
    - Enum: True, False, Unknown
    -
    true
    typestring - type of condition in CamelCase or in foo.example.com/CamelCase.
    -
    true
    observedGenerationinteger - observedGeneration represents the .metadata.generation that the condition was set based upon. -For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date -with respect to the current state of the instance.
    -
    - Format: int64
    - Minimum: 0
    -
    false
    - - -### PlatformInvitation.status.email -[↩ Parent](#platforminvitationstatus) - - - -The email resource that was created for the platform invitation. - - - - - - - - - - - - - - - - - - - - - -
    NameTypeDescriptionRequired
    namestring - The name of the email resource that was created for the platform invitation.
    -
    false
    namespacestring - The namespace of the email resource that was created for the platform invitation.
    -
    false
    - ## PolicyBinding [↩ Parent](#iammiloapiscomv1alpha1 ) diff --git a/pkg/apis/iam/v1alpha1/platformaccessapproval_types.go b/pkg/apis/iam/v1alpha1/platformaccessapproval_types.go deleted file mode 100644 index 984a7956..00000000 --- a/pkg/apis/iam/v1alpha1/platformaccessapproval_types.go +++ /dev/null @@ -1,69 +0,0 @@ -package v1alpha1 - -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - -// Create conditions -const ( - // PlatformAccessApprovalReadyCondition is the condition Type that tracks platform access approval creation status. - PlatformAccessApprovalReadyCondition = "Ready" - // PlatformAccessApprovalReconciledReason is used when platform access approval reconciliation succeeds. - PlatformAccessApprovalReconciledReason = "Reconciled" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status - -// PlatformAccessApproval is the Schema for the platformaccessapprovals API. -// It represents a platform access approval for a user. Once the platform access approval is created, an email will be sent to the user. -// +kubebuilder:resource:scope=Cluster -type PlatformAccessApproval struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec PlatformAccessApprovalSpec `json:"spec,omitempty"` - Status PlatformAccessApprovalStatus `json:"status,omitempty"` -} - -// PlatformAccessApprovalSpec defines the desired state of PlatformAccessApproval. -// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="spec is immutable" -// +kubebuilder:validation:Type=object -type PlatformAccessApprovalSpec struct { - // SubjectRef is the reference to the subject being approved. - // +kubebuilder:validation:Required - SubjectRef SubjectReference `json:"subjectRef"` - // ApproverRef is the reference to the approver being approved. - // If not specified, the approval was made by the system. - // +kubebuilder:validation:Optional - ApproverRef *UserReference `json:"approverRef,omitempty"` -} - -// +kubebuilder:object:root=true - -// PlatformAccessApprovalList contains a list of PlatformAccessApproval. -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type PlatformAccessApprovalList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []PlatformAccessApproval `json:"items"` -} - -type PlatformAccessApprovalStatus struct { - // Conditions provide conditions that represent the current status of the PlatformAccessApproval. - // +kubebuilder:default={{type: "Ready", status: "Unknown", reason: "ReconcilePending", message: "Platform access approval reconciliation is pending", lastTransitionTime: "1970-01-01T00:00:00Z"}} - // +kubebuilder:validation:Optional - Conditions []metav1.Condition `json:"conditions,omitempty"` -} - -// +kubebuilder:validation:XValidation:rule="(has(self.email) && !has(self.userRef)) || (!has(self.email) && has(self.userRef))",message="Exactly one of email or userRef must be specified" -type SubjectReference struct { - // Email is the email of the user being approved. - // Use Email to approve an email address that is not associated with a created user. (e.g. when using PlatformInvitation) - // UserRef and Email are mutually exclusive. Exactly one of them must be specified. - // +kubebuilder:validation:Optional - Email string `json:"email,omitempty"` - // UserRef is the reference to the user being approved. - // UserRef and Email are mutually exclusive. Exactly one of them must be specified. - // +kubebuilder:validation:Optional - UserRef *UserReference `json:"userRef,omitempty"` -} diff --git a/pkg/apis/iam/v1alpha1/platforminvitation_types.go b/pkg/apis/iam/v1alpha1/platforminvitation_types.go deleted file mode 100644 index 23d8e7a1..00000000 --- a/pkg/apis/iam/v1alpha1/platforminvitation_types.go +++ /dev/null @@ -1,90 +0,0 @@ -package v1alpha1 - -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - -// Create conditions -const ( - // PlatformInvitationReadyCondition is the condition Type that tracks platform invitation creation status. - PlatformInvitationReadyCondition = "Ready" - // PlatformInvitationReconciledReason is used when platform invitation reconciliation succeeds. - PlatformInvitationReconciledReason = "Reconciled" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status - -// PlatformInvitation is the Schema for the platforminvitations API -// It represents a platform invitation for a user. Once the platform invitation is created, an email will be sent to the user to invite them to the platform. -// The invited user will have access to the platform after they create an account using the asociated email. -// +kubebuilder:printcolumn:name="Email",type=string,JSONPath=".spec.email" -// +kubebuilder:printcolumn:name="Schedule At",type="string",JSONPath=".spec.scheduleAt" -// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" -// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=".metadata.creationTimestamp" -// It represents a platform invitation for a user. -// +kubebuilder:resource:scope=Cluster -type PlatformInvitation struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec PlatformInvitationSpec `json:"spec,omitempty"` - Status PlatformInvitationStatus `json:"status,omitempty"` -} - -// PlatformInvitationSpec defines the desired state of PlatformInvitation. -// +kubebuilder:validation:Type=object -type PlatformInvitationSpec struct { - // The email of the user being invited. - // +kubebuilder:validation:Required - // +kubebuilder:validation:XValidation:rule="type(oldSelf) == null_type || self == oldSelf",message="email type is immutable" - Email string `json:"email"` - - // The given name of the user being invited. - // +kubebuilder:validation:Optional - GivenName string `json:"givenName,omitempty"` - - // The family name of the user being invited. - // +kubebuilder:validation:Optional - FamilyName string `json:"familyName,omitempty"` - - // The schedule at which the platform invitation will be sent. - // It can only be updated before the platform invitation is sent. - // +kubebuilder:validation:Optional - ScheduleAt *metav1.Time `json:"scheduleAt,omitempty"` - - // The user who created the platform invitation. A mutation webhook will default this field to the user who made the request. - // +kubebuilder:validation:XValidation:rule="type(oldSelf) == null_type || self == oldSelf",message="invitedBy type is immutable" - InvitedBy UserReference `json:"invitedBy,omitempty"` -} - -// +kubebuilder:object:root=true - -// PlatformInvitationList contains a list of PlatformInvitation. -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type PlatformInvitationList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []PlatformInvitation `json:"items"` -} - -// PlatformInvitationStatus defines the observed state of PlatformInvitation. -// +kubebuilder:validation:Type=object -type PlatformInvitationStatus struct { - // Conditions provide conditions that represent the current status of the PlatformInvitation. - // +kubebuilder:default={{type: "Ready", status: "Unknown", reason: "ReconcilePending", message: "Platform invitation reconciliation is pending", lastTransitionTime: "1970-01-01T00:00:00Z"}} - // +kubebuilder:validation:Optional - Conditions []metav1.Condition `json:"conditions,omitempty"` - - // The email resource that was created for the platform invitation. - // +kubebuilder:validation:Optional - Email PlatformInvitationEmailStatus `json:"email,omitempty"` -} - -type PlatformInvitationEmailStatus struct { - // The name of the email resource that was created for the platform invitation. - // +kubebuilder:validation:Optional - Name string `json:"name,omitempty"` - // The namespace of the email resource that was created for the platform invitation. - // +kubebuilder:validation:Optional - Namespace string `json:"namespace,omitempty"` -} diff --git a/pkg/apis/iam/v1alpha1/register.go b/pkg/apis/iam/v1alpha1/register.go index 20b75502..35cae7ad 100644 --- a/pkg/apis/iam/v1alpha1/register.go +++ b/pkg/apis/iam/v1alpha1/register.go @@ -43,10 +43,6 @@ func addKnownTypes(scheme *runtime.Scheme) error { &UserDeactivationList{}, &UserInvitation{}, &UserInvitationList{}, - &PlatformInvitation{}, - &PlatformInvitationList{}, - &PlatformAccessApproval{}, - &PlatformAccessApprovalList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/iam/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iam/v1alpha1/zz_generated.deepcopy.go index 0d44a181..e5b93a4d 100644 --- a/pkg/apis/iam/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/iam/v1alpha1/zz_generated.deepcopy.go @@ -413,225 +413,6 @@ func (in *ParentResourceRef) DeepCopy() *ParentResourceRef { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformAccessApproval) DeepCopyInto(out *PlatformAccessApproval) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAccessApproval. -func (in *PlatformAccessApproval) DeepCopy() *PlatformAccessApproval { - if in == nil { - return nil - } - out := new(PlatformAccessApproval) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *PlatformAccessApproval) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformAccessApprovalList) DeepCopyInto(out *PlatformAccessApprovalList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]PlatformAccessApproval, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAccessApprovalList. -func (in *PlatformAccessApprovalList) DeepCopy() *PlatformAccessApprovalList { - if in == nil { - return nil - } - out := new(PlatformAccessApprovalList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *PlatformAccessApprovalList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformAccessApprovalSpec) DeepCopyInto(out *PlatformAccessApprovalSpec) { - *out = *in - in.SubjectRef.DeepCopyInto(&out.SubjectRef) - if in.ApproverRef != nil { - in, out := &in.ApproverRef, &out.ApproverRef - *out = new(UserReference) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAccessApprovalSpec. -func (in *PlatformAccessApprovalSpec) DeepCopy() *PlatformAccessApprovalSpec { - if in == nil { - return nil - } - out := new(PlatformAccessApprovalSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformAccessApprovalStatus) DeepCopyInto(out *PlatformAccessApprovalStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformAccessApprovalStatus. -func (in *PlatformAccessApprovalStatus) DeepCopy() *PlatformAccessApprovalStatus { - if in == nil { - return nil - } - out := new(PlatformAccessApprovalStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformInvitation) DeepCopyInto(out *PlatformInvitation) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitation. -func (in *PlatformInvitation) DeepCopy() *PlatformInvitation { - if in == nil { - return nil - } - out := new(PlatformInvitation) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *PlatformInvitation) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformInvitationEmailStatus) DeepCopyInto(out *PlatformInvitationEmailStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitationEmailStatus. -func (in *PlatformInvitationEmailStatus) DeepCopy() *PlatformInvitationEmailStatus { - if in == nil { - return nil - } - out := new(PlatformInvitationEmailStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformInvitationList) DeepCopyInto(out *PlatformInvitationList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]PlatformInvitation, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitationList. -func (in *PlatformInvitationList) DeepCopy() *PlatformInvitationList { - if in == nil { - return nil - } - out := new(PlatformInvitationList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *PlatformInvitationList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformInvitationSpec) DeepCopyInto(out *PlatformInvitationSpec) { - *out = *in - if in.ScheduleAt != nil { - in, out := &in.ScheduleAt, &out.ScheduleAt - *out = (*in).DeepCopy() - } - out.InvitedBy = in.InvitedBy -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitationSpec. -func (in *PlatformInvitationSpec) DeepCopy() *PlatformInvitationSpec { - if in == nil { - return nil - } - out := new(PlatformInvitationSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PlatformInvitationStatus) DeepCopyInto(out *PlatformInvitationStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - out.Email = in.Email -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformInvitationStatus. -func (in *PlatformInvitationStatus) DeepCopy() *PlatformInvitationStatus { - if in == nil { - return nil - } - out := new(PlatformInvitationStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PolicyBinding) DeepCopyInto(out *PolicyBinding) { *out = *in @@ -1063,26 +844,6 @@ func (in *Subject) DeepCopy() *Subject { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SubjectReference) DeepCopyInto(out *SubjectReference) { - *out = *in - if in.UserRef != nil { - in, out := &in.UserRef, &out.UserRef - *out = new(UserReference) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SubjectReference. -func (in *SubjectReference) DeepCopy() *SubjectReference { - if in == nil { - return nil - } - out := new(SubjectReference) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *User) DeepCopyInto(out *User) { *out = *in