Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
3c569bb
feat: implement DocumentController with reconciliation logic for Docu…
JoseSzycho Oct 14, 2025
a886055
feat: add DocumentRevisionController with reconciliation logic and un…
JoseSzycho Oct 15, 2025
e9bae54
feat: integrate Document and DocumentRevision controllers into the co…
JoseSzycho Oct 15, 2025
6162c72
Merge branch 'integration/agreements-api' into feat/agreements-contro…
JoseSzycho Oct 15, 2025
377b1d7
Merge branch 'integration/agreements-api' into feat/agreements-contro…
JoseSzycho Oct 16, 2025
9edbc7c
fix: add Agreement API to controller manager initialization
JoseSzycho Oct 16, 2025
ab9708c
fix: update kustomization to include Agreement and Documentation bases
JoseSzycho Oct 16, 2025
bc76ee1
Merge branch 'integration/agreements-api' into feat/agreements-contro…
JoseSzycho Oct 16, 2025
a50724f
Merge branch 'integration/agreements-api' into feat/agreements-contro…
JoseSzycho Oct 16, 2025
cbcf028
fix: update webhook paths for document and documentrevision resources
JoseSzycho Oct 16, 2025
0796422
fix: enhance UserInvitationMutator to retrieve inviter user details f…
JoseSzycho Oct 17, 2025
75fb8a3
fix: update ContactMutator to handle SubjectRef changes and adjust ow…
JoseSzycho Oct 17, 2025
debad54
Revert "fix: update ContactMutator to handle SubjectRef changes and a…
JoseSzycho Oct 17, 2025
269f325
feat: add PlatformAccessApproval and PlatformInvitation CRDs with sam…
JoseSzycho Oct 21, 2025
3225f25
Revert commit in wrong branch "feat: add PlatformAccessApproval and P…
JoseSzycho Oct 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions cmd/milo/controller-manager/controllermanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -82,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"
Expand Down Expand Up @@ -132,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 (
Expand Down Expand Up @@ -506,6 +509,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions config/crd/overlays/core-control-plane/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ resources:
- ../../bases/iam/
- ../../bases/resourcemanager/
- ../../bases/notification/
- ../../bases/agreement/
- ../../bases/documentation/
4 changes: 2 additions & 2 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
188 changes: 188 additions & 0 deletions internal/controllers/documentation/document_controller.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading
Loading