From b11dd4f91f61f31ddf4c3308d548041dc0389cc4 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 20:29:26 +0000 Subject: [PATCH 1/9] feat: add tenant features with argo workflows rbac --- apis/management/functions/xtenant/fn.go | 33 ++++ apis/management/functions/xtenant/fn_test.go | 77 ++++++++ .../package/apis/tenant/definition.yaml | 12 ++ apis/pkg/resources/rbac.go | 184 ++++++++++++++++++ apis/pkg/resources/rbac_test.go | 144 ++++++++++++++ apis/pkg/resources/secret.go | 70 +++++++ apis/pkg/resources/secret_test.go | 67 +++++++ 7 files changed, 587 insertions(+) create mode 100644 apis/pkg/resources/rbac.go create mode 100644 apis/pkg/resources/rbac_test.go create mode 100644 apis/pkg/resources/secret.go create mode 100644 apis/pkg/resources/secret_test.go diff --git a/apis/management/functions/xtenant/fn.go b/apis/management/functions/xtenant/fn.go index 45f44e7..c365c64 100644 --- a/apis/management/functions/xtenant/fn.go +++ b/apis/management/functions/xtenant/fn.go @@ -12,6 +12,7 @@ import ( "github.com/crossplane/function-sdk-go/resource" "github.com/crossplane/function-sdk-go/resource/composed" "github.com/crossplane/function-sdk-go/response" + rbacv1 "k8s.io/api/rbac/v1" ) // Function implements the Crossplane composition function for Tenant resources. @@ -56,6 +57,30 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) "project": project, } + if argoWorkflowsEnabled(oxr) { + desiredTyped["argo-workflow-sa"] = resources.NewServiceAccount(resources.ServiceAccountConfig{ + Name: "argo-workflow", + Namespace: tenantName, + }) + desiredTyped["argo-workflow-role"] = resources.NewRole(resources.RoleConfig{ + Name: "argo-workflow", + Namespace: tenantName, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"argoproj.io"}, + Resources: []string{"workflowtaskresults"}, + Verbs: []string{"create", "patch"}, + }, + }, + }) + desiredTyped["argo-workflow-rolebinding"] = resources.NewRoleBinding(resources.RoleBindingConfig{ + Name: "argo-workflow", + Namespace: tenantName, + RoleName: "argo-workflow", + ServiceAccountName: "argo-workflow", + }) + } + desired, err := request.GetDesiredComposedResources(req) if err != nil { response.Fatal(rsp, errors.Wrap(err, "cannot get desired composed resources")) @@ -81,3 +106,11 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) return rsp, nil } + +func argoWorkflowsEnabled(oxr *resource.Composite) bool { + enabled, err := oxr.Resource.GetBool("spec.features.argoWorkflows.enabled") + if err != nil { + return false + } + return enabled +} diff --git a/apis/management/functions/xtenant/fn_test.go b/apis/management/functions/xtenant/fn_test.go index f94c32b..f1bcd09 100644 --- a/apis/management/functions/xtenant/fn_test.go +++ b/apis/management/functions/xtenant/fn_test.go @@ -141,6 +141,83 @@ func TestRunFunction(t *testing.T) { }, }, }, + "ArgoWorkflowsEnabled": { + reason: "The function should create RBAC resources when argoWorkflows is enabled", + args: args{ + req: &fnv1.RunFunctionRequest{ + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{ + "apiVersion": "platform.synapse.io/v1alpha1", + "kind": "Tenant", + "metadata": {"name": "ml"}, + "spec": { + "name": "ml", + "sourceRepos": ["https://github.com/org/ml-apps.git"], + "features": { + "argoWorkflows": { + "enabled": true + } + } + } + }`), + }, + }, + }, + }, + wantResourceCnt: 5, + validateFn: func(t *testing.T, rsp *fnv1.RunFunctionResponse) { + t.Helper() + resources := rsp.GetDesired().GetResources() + + sa := resources["argo-workflow-sa"] + if sa == nil { + t.Fatal("expected argo-workflow-sa resource") + } + saData := structToMap(t, sa.GetResource()) + assertEqual(t, "ml-argo-workflow-sa", getNestedString(saData, "metadata", "name")) + + role := resources["argo-workflow-role"] + if role == nil { + t.Fatal("expected argo-workflow-role resource") + } + roleData := structToMap(t, role.GetResource()) + assertEqual(t, "ml-argo-workflow-role", getNestedString(roleData, "metadata", "name")) + + rb := resources["argo-workflow-rolebinding"] + if rb == nil { + t.Fatal("expected argo-workflow-rolebinding resource") + } + rbData := structToMap(t, rb.GetResource()) + assertEqual(t, "ml-argo-workflow-rolebinding", getNestedString(rbData, "metadata", "name")) + }, + }, + "ArgoWorkflowsDisabled": { + reason: "The function should not create RBAC resources when argoWorkflows is disabled", + args: args{ + req: &fnv1.RunFunctionRequest{ + Observed: &fnv1.State{ + Composite: &fnv1.Resource{ + Resource: resource.MustStructJSON(`{ + "apiVersion": "platform.synapse.io/v1alpha1", + "kind": "Tenant", + "metadata": {"name": "ml"}, + "spec": { + "name": "ml", + "sourceRepos": ["https://github.com/org/ml-apps.git"], + "features": { + "argoWorkflows": { + "enabled": false + } + } + } + }`), + }, + }, + }, + }, + wantResourceCnt: 2, + }, } for name, tc := range cases { diff --git a/apis/management/package/apis/tenant/definition.yaml b/apis/management/package/apis/tenant/definition.yaml index 620a9a5..2251045 100644 --- a/apis/management/package/apis/tenant/definition.yaml +++ b/apis/management/package/apis/tenant/definition.yaml @@ -33,6 +33,18 @@ spec: description: Git repositories the tenant can deploy from items: type: string + features: + type: object + description: Optional features to enable for this tenant + properties: + argoWorkflows: + type: object + description: Argo Workflows integration + properties: + enabled: + type: boolean + description: Enable Argo Workflows RBAC for this tenant + default: false status: type: object properties: diff --git a/apis/pkg/resources/rbac.go b/apis/pkg/resources/rbac.go new file mode 100644 index 0000000..afb83aa --- /dev/null +++ b/apis/pkg/resources/rbac.go @@ -0,0 +1,184 @@ +package resources + +import ( + kubeobj "github.com/crossplane-contrib/provider-kubernetes/apis/cluster/object/v1alpha2" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ServiceAccountConfig configures a ServiceAccount. +type ServiceAccountConfig struct { + Name string + Namespace string + Labels map[string]string + Annotations map[string]string + ProviderCfgName string +} + +func (c *ServiceAccountConfig) withDefaults() { + if c.ProviderCfgName == "" { + c.ProviderCfgName = DefaultProviderCfg + } + if c.Labels == nil { + c.Labels = make(map[string]string) + } +} + +// NewServiceAccount creates a provider-kubernetes Object that manages a ServiceAccount. +func NewServiceAccount(cfg ServiceAccountConfig) *kubeobj.Object { + cfg.withDefaults() + + return &kubeobj.Object{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kubernetes.crossplane.io/v1alpha2", + Kind: "Object", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Namespace + "-" + cfg.Name + "-sa", + }, + Spec: kubeobj.ObjectSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{Name: cfg.ProviderCfgName}, + }, + ForProvider: kubeobj.ObjectParameters{ + Manifest: runtime.RawExtension{ + Object: &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ServiceAccount", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Name, + Namespace: cfg.Namespace, + Labels: cfg.Labels, + Annotations: cfg.Annotations, + }, + }, + }, + }, + }, + } +} + +// RoleConfig configures a Role. +type RoleConfig struct { + Name string + Namespace string + Labels map[string]string + Rules []rbacv1.PolicyRule + ProviderCfgName string +} + +func (c *RoleConfig) withDefaults() { + if c.ProviderCfgName == "" { + c.ProviderCfgName = DefaultProviderCfg + } + if c.Labels == nil { + c.Labels = make(map[string]string) + } +} + +// NewRole creates a provider-kubernetes Object that manages a Role. +func NewRole(cfg RoleConfig) *kubeobj.Object { + cfg.withDefaults() + + return &kubeobj.Object{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kubernetes.crossplane.io/v1alpha2", + Kind: "Object", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Namespace + "-" + cfg.Name + "-role", + }, + Spec: kubeobj.ObjectSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{Name: cfg.ProviderCfgName}, + }, + ForProvider: kubeobj.ObjectParameters{ + Manifest: runtime.RawExtension{ + Object: &rbacv1.Role{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rbac.authorization.k8s.io/v1", + Kind: "Role", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Name, + Namespace: cfg.Namespace, + Labels: cfg.Labels, + }, + Rules: cfg.Rules, + }, + }, + }, + }, + } +} + +// RoleBindingConfig configures a RoleBinding. +type RoleBindingConfig struct { + Name string + Namespace string + Labels map[string]string + RoleName string + ServiceAccountName string + ProviderCfgName string +} + +func (c *RoleBindingConfig) withDefaults() { + if c.ProviderCfgName == "" { + c.ProviderCfgName = DefaultProviderCfg + } + if c.Labels == nil { + c.Labels = make(map[string]string) + } +} + +// NewRoleBinding creates a provider-kubernetes Object that manages a RoleBinding. +func NewRoleBinding(cfg RoleBindingConfig) *kubeobj.Object { + cfg.withDefaults() + + return &kubeobj.Object{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kubernetes.crossplane.io/v1alpha2", + Kind: "Object", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Namespace + "-" + cfg.Name + "-rolebinding", + }, + Spec: kubeobj.ObjectSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{Name: cfg.ProviderCfgName}, + }, + ForProvider: kubeobj.ObjectParameters{ + Manifest: runtime.RawExtension{ + Object: &rbacv1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rbac.authorization.k8s.io/v1", + Kind: "RoleBinding", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Name, + Namespace: cfg.Namespace, + Labels: cfg.Labels, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: cfg.RoleName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: cfg.ServiceAccountName, + Namespace: cfg.Namespace, + }, + }, + }, + }, + }, + }, + } +} diff --git a/apis/pkg/resources/rbac_test.go b/apis/pkg/resources/rbac_test.go new file mode 100644 index 0000000..a0dc8e9 --- /dev/null +++ b/apis/pkg/resources/rbac_test.go @@ -0,0 +1,144 @@ +package resources + +import ( + "testing" + + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestNewServiceAccount(t *testing.T) { + tests := []struct { + name string + cfg ServiceAccountConfig + wantName string + }{ + { + name: "basic", + cfg: ServiceAccountConfig{ + Name: "argo-workflow", + Namespace: "finance", + }, + wantName: "finance-argo-workflow-sa", + }, + { + name: "custom provider", + cfg: ServiceAccountConfig{ + Name: "argo-workflow", + Namespace: "finance", + ProviderCfgName: "custom", + }, + wantName: "finance-argo-workflow-sa", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sa := NewServiceAccount(tt.cfg) + if sa.Name != tt.wantName { + t.Errorf("expected name %s, got %s", tt.wantName, sa.Name) + } + if sa.Spec.ForProvider.Manifest.Object == nil { + t.Fatal("expected manifest object") + } + if tt.cfg.ProviderCfgName != "" && sa.Spec.ProviderConfigReference.Name != tt.cfg.ProviderCfgName { + t.Errorf("expected provider %s, got %s", tt.cfg.ProviderCfgName, sa.Spec.ProviderConfigReference.Name) + } + }) + } +} + +func TestNewRole(t *testing.T) { + rules := []rbacv1.PolicyRule{ + { + APIGroups: []string{"argoproj.io"}, + Resources: []string{"workflowtaskresults"}, + Verbs: []string{"create", "patch"}, + }, + } + + tests := []struct { + name string + cfg RoleConfig + wantName string + }{ + { + name: "basic", + cfg: RoleConfig{ + Name: "argo-workflow", + Namespace: "finance", + Rules: rules, + }, + wantName: "finance-argo-workflow-role", + }, + { + name: "custom provider", + cfg: RoleConfig{ + Name: "argo-workflow", + Namespace: "finance", + Rules: rules, + ProviderCfgName: "custom", + }, + wantName: "finance-argo-workflow-role", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + role := NewRole(tt.cfg) + if role.Name != tt.wantName { + t.Errorf("expected name %s, got %s", tt.wantName, role.Name) + } + if role.Spec.ForProvider.Manifest.Object == nil { + t.Fatal("expected manifest object") + } + if tt.cfg.ProviderCfgName != "" && role.Spec.ProviderConfigReference.Name != tt.cfg.ProviderCfgName { + t.Errorf("expected provider %s, got %s", tt.cfg.ProviderCfgName, role.Spec.ProviderConfigReference.Name) + } + }) + } +} + +func TestNewRoleBinding(t *testing.T) { + tests := []struct { + name string + cfg RoleBindingConfig + wantName string + }{ + { + name: "basic", + cfg: RoleBindingConfig{ + Name: "argo-workflow", + Namespace: "finance", + RoleName: "argo-workflow", + ServiceAccountName: "argo-workflow", + }, + wantName: "finance-argo-workflow-rolebinding", + }, + { + name: "custom provider", + cfg: RoleBindingConfig{ + Name: "argo-workflow", + Namespace: "finance", + RoleName: "argo-workflow", + ServiceAccountName: "argo-workflow", + ProviderCfgName: "custom", + }, + wantName: "finance-argo-workflow-rolebinding", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rb := NewRoleBinding(tt.cfg) + if rb.Name != tt.wantName { + t.Errorf("expected name %s, got %s", tt.wantName, rb.Name) + } + if rb.Spec.ForProvider.Manifest.Object == nil { + t.Fatal("expected manifest object") + } + if tt.cfg.ProviderCfgName != "" && rb.Spec.ProviderConfigReference.Name != tt.cfg.ProviderCfgName { + t.Errorf("expected provider %s, got %s", tt.cfg.ProviderCfgName, rb.Spec.ProviderConfigReference.Name) + } + }) + } +} diff --git a/apis/pkg/resources/secret.go b/apis/pkg/resources/secret.go new file mode 100644 index 0000000..5cd2ae9 --- /dev/null +++ b/apis/pkg/resources/secret.go @@ -0,0 +1,70 @@ +package resources + +import ( + kubeobj "github.com/crossplane-contrib/provider-kubernetes/apis/cluster/object/v1alpha2" + xpv1 "github.com/crossplane/crossplane-runtime/v2/apis/common/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// SecretConfig configures a Secret. +type SecretConfig struct { + Name string + Namespace string + Labels map[string]string + Annotations map[string]string + StringData map[string]string + Type corev1.SecretType + ProviderCfgName string +} + +func (c *SecretConfig) withDefaults() { + if c.ProviderCfgName == "" { + c.ProviderCfgName = DefaultProviderCfg + } + if c.Labels == nil { + c.Labels = make(map[string]string) + } + if c.Type == "" { + c.Type = corev1.SecretTypeOpaque + } +} + +// NewSecret creates a provider-kubernetes Object that manages a Secret. +func NewSecret(cfg SecretConfig) *kubeobj.Object { + cfg.withDefaults() + + return &kubeobj.Object{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "kubernetes.crossplane.io/v1alpha2", + Kind: "Object", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Namespace + "-" + cfg.Name + "-secret", + }, + Spec: kubeobj.ObjectSpec{ + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{Name: cfg.ProviderCfgName}, + }, + ForProvider: kubeobj.ObjectParameters{ + Manifest: runtime.RawExtension{ + Object: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Name, + Namespace: cfg.Namespace, + Labels: cfg.Labels, + Annotations: cfg.Annotations, + }, + StringData: cfg.StringData, + Type: cfg.Type, + }, + }, + }, + }, + } +} diff --git a/apis/pkg/resources/secret_test.go b/apis/pkg/resources/secret_test.go new file mode 100644 index 0000000..8fb87f0 --- /dev/null +++ b/apis/pkg/resources/secret_test.go @@ -0,0 +1,67 @@ +package resources + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" +) + +func TestNewSecret(t *testing.T) { + tests := []struct { + name string + cfg SecretConfig + wantName string + }{ + { + name: "basic", + cfg: SecretConfig{ + Name: "artifacts-s3", + Namespace: "finance", + StringData: map[string]string{ + "accessKey": "test-key", + "secretKey": "test-secret", + }, + }, + wantName: "finance-artifacts-s3-secret", + }, + { + name: "custom type", + cfg: SecretConfig{ + Name: "artifacts-s3", + Namespace: "finance", + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + "accessKey": "test-key", + }, + }, + wantName: "finance-artifacts-s3-secret", + }, + { + name: "custom provider", + cfg: SecretConfig{ + Name: "artifacts-s3", + Namespace: "finance", + ProviderCfgName: "custom", + StringData: map[string]string{ + "accessKey": "test-key", + }, + }, + wantName: "finance-artifacts-s3-secret", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + secret := NewSecret(tt.cfg) + if secret.Name != tt.wantName { + t.Errorf("expected name %s, got %s", tt.wantName, secret.Name) + } + if secret.Spec.ForProvider.Manifest.Object == nil { + t.Fatal("expected manifest object") + } + if tt.cfg.ProviderCfgName != "" && secret.Spec.ProviderConfigReference.Name != tt.cfg.ProviderCfgName { + t.Errorf("expected provider %s, got %s", tt.cfg.ProviderCfgName, secret.Spec.ProviderConfigReference.Name) + } + }) + } +} From faece2e54f71476290a4e921e1d52c59f76d11b4 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 20:37:24 +0000 Subject: [PATCH 2/9] fix: import ordering for gci linter --- apis/management/functions/xtenant/fn.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apis/management/functions/xtenant/fn.go b/apis/management/functions/xtenant/fn.go index c365c64..d454e69 100644 --- a/apis/management/functions/xtenant/fn.go +++ b/apis/management/functions/xtenant/fn.go @@ -3,8 +3,6 @@ package main import ( "context" - "github.com/mdaops/cortex/configurations/pkg/resources" - "github.com/crossplane/function-sdk-go/errors" "github.com/crossplane/function-sdk-go/logging" fnv1 "github.com/crossplane/function-sdk-go/proto/v1" @@ -13,6 +11,8 @@ import ( "github.com/crossplane/function-sdk-go/resource/composed" "github.com/crossplane/function-sdk-go/response" rbacv1 "k8s.io/api/rbac/v1" + + "github.com/mdaops/cortex/configurations/pkg/resources" ) // Function implements the Crossplane composition function for Tenant resources. From 9285c8eb7411483346a9978a6304fe8f6609a4fd Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 20:52:48 +0000 Subject: [PATCH 3/9] fix: import ordering per gci config --- apis/management/functions/xtenant/fn.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apis/management/functions/xtenant/fn.go b/apis/management/functions/xtenant/fn.go index d454e69..796bdcf 100644 --- a/apis/management/functions/xtenant/fn.go +++ b/apis/management/functions/xtenant/fn.go @@ -3,6 +3,9 @@ package main import ( "context" + "github.com/mdaops/cortex/configurations/pkg/resources" + rbacv1 "k8s.io/api/rbac/v1" + "github.com/crossplane/function-sdk-go/errors" "github.com/crossplane/function-sdk-go/logging" fnv1 "github.com/crossplane/function-sdk-go/proto/v1" @@ -10,9 +13,6 @@ import ( "github.com/crossplane/function-sdk-go/resource" "github.com/crossplane/function-sdk-go/resource/composed" "github.com/crossplane/function-sdk-go/response" - rbacv1 "k8s.io/api/rbac/v1" - - "github.com/mdaops/cortex/configurations/pkg/resources" ) // Function implements the Crossplane composition function for Tenant resources. From 1cdd569270787ce75ac549dacf95a78a6505faa0 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 21:05:12 +0000 Subject: [PATCH 4/9] refactor: extract tenantComposer struct with receivers --- apis/management/functions/xtenant/fn.go | 127 +++++++++++++++--------- 1 file changed, 81 insertions(+), 46 deletions(-) diff --git a/apis/management/functions/xtenant/fn.go b/apis/management/functions/xtenant/fn.go index 796bdcf..fc505aa 100644 --- a/apis/management/functions/xtenant/fn.go +++ b/apis/management/functions/xtenant/fn.go @@ -22,6 +22,80 @@ type Function struct { log logging.Logger } +// tenantComposer holds state for composing tenant resources. +type tenantComposer struct { + oxr *resource.Composite + name string + desired map[resource.Name]any +} + +func newTenantComposer(oxr *resource.Composite) (*tenantComposer, error) { + name, err := oxr.Resource.GetString("spec.name") + if err != nil { + return nil, errors.Wrap(err, "spec.name is required") + } + return &tenantComposer{ + oxr: oxr, + name: name, + desired: make(map[resource.Name]any), + }, nil +} + +func (tc *tenantComposer) getString(path string) string { + val, _ := tc.oxr.Resource.GetString(path) + return val +} + +func (tc *tenantComposer) getStringArray(path string) []string { + val, _ := tc.oxr.Resource.GetStringArray(path) + return val +} + +func (tc *tenantComposer) featureEnabled(feature string) bool { + enabled, err := tc.oxr.Resource.GetBool("spec.features." + feature + ".enabled") + return err == nil && enabled +} + +func (tc *tenantComposer) add(name resource.Name, obj any) { + tc.desired[name] = obj +} + +func (tc *tenantComposer) composeBase() error { + tc.add("namespace", resources.NewTenantNamespace(tc.name)) + + project, err := resources.NewTenantProject(tc.name, tc.getString("spec.description"), tc.getStringArray("spec.sourceRepos")) + if err != nil { + return errors.Wrap(err, "spec.sourceRepos is required") + } + tc.add("project", project) + return nil +} + +func (tc *tenantComposer) composeArgoWorkflows() { + if !tc.featureEnabled("argoWorkflows") { + return + } + tc.add("argo-workflow-sa", resources.NewServiceAccount(resources.ServiceAccountConfig{ + Name: "argo-workflow", + Namespace: tc.name, + })) + tc.add("argo-workflow-role", resources.NewRole(resources.RoleConfig{ + Name: "argo-workflow", + Namespace: tc.name, + Rules: []rbacv1.PolicyRule{{ + APIGroups: []string{"argoproj.io"}, + Resources: []string{"workflowtaskresults"}, + Verbs: []string{"create", "patch"}, + }}, + })) + tc.add("argo-workflow-rolebinding", resources.NewRoleBinding(resources.RoleBindingConfig{ + Name: "argo-workflow", + Namespace: tc.name, + RoleName: "argo-workflow", + ServiceAccountName: "argo-workflow", + })) +} + // RunFunction composes namespace and ArgoCD project resources for a Tenant. func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) { rsp := response.To(req, response.DefaultTTL) @@ -37,49 +111,18 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) "xr-kind", oxr.Resource.GetKind(), ) - tenantName, err := oxr.Resource.GetString("spec.name") + tc, err := newTenantComposer(oxr) if err != nil { - response.Fatal(rsp, errors.Wrap(err, "spec.name is required")) + response.Fatal(rsp, err) return rsp, nil } - description, _ := oxr.Resource.GetString("spec.description") - sourceRepos, _ := oxr.Resource.GetStringArray("spec.sourceRepos") - - project, err := resources.NewTenantProject(tenantName, description, sourceRepos) - if err != nil { - response.Fatal(rsp, errors.Wrap(err, "spec.sourceRepos is required")) + if err := tc.composeBase(); err != nil { + response.Fatal(rsp, err) return rsp, nil } - desiredTyped := map[resource.Name]any{ - "namespace": resources.NewTenantNamespace(tenantName), - "project": project, - } - - if argoWorkflowsEnabled(oxr) { - desiredTyped["argo-workflow-sa"] = resources.NewServiceAccount(resources.ServiceAccountConfig{ - Name: "argo-workflow", - Namespace: tenantName, - }) - desiredTyped["argo-workflow-role"] = resources.NewRole(resources.RoleConfig{ - Name: "argo-workflow", - Namespace: tenantName, - Rules: []rbacv1.PolicyRule{ - { - APIGroups: []string{"argoproj.io"}, - Resources: []string{"workflowtaskresults"}, - Verbs: []string{"create", "patch"}, - }, - }, - }) - desiredTyped["argo-workflow-rolebinding"] = resources.NewRoleBinding(resources.RoleBindingConfig{ - Name: "argo-workflow", - Namespace: tenantName, - RoleName: "argo-workflow", - ServiceAccountName: "argo-workflow", - }) - } + tc.composeArgoWorkflows() desired, err := request.GetDesiredComposedResources(req) if err != nil { @@ -87,7 +130,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) return rsp, nil } - for name, obj := range desiredTyped { + for name, obj := range tc.desired { c := composed.New() if err := resources.ConvertViaJSON(c, obj); err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot convert %s to unstructured", name)) @@ -101,16 +144,8 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) return rsp, nil } - log.Info("Composed tenant resources", "tenant", tenantName) + log.Info("Composed tenant resources", "tenant", tc.name) response.ConditionTrue(rsp, "FunctionSuccess", "Success").TargetCompositeAndClaim() return rsp, nil } - -func argoWorkflowsEnabled(oxr *resource.Composite) bool { - enabled, err := oxr.Resource.GetBool("spec.features.argoWorkflows.enabled") - if err != nil { - return false - } - return enabled -} From 8b610e26bf9669afacb5fe97018ddb35ae211c37 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 21:11:46 +0000 Subject: [PATCH 5/9] refactor: rename to composer, simplify --- apis/management/functions/xtenant/fn.go | 86 ++++++++++++------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/apis/management/functions/xtenant/fn.go b/apis/management/functions/xtenant/fn.go index fc505aa..5f0ae88 100644 --- a/apis/management/functions/xtenant/fn.go +++ b/apis/management/functions/xtenant/fn.go @@ -22,75 +22,79 @@ type Function struct { log logging.Logger } -// tenantComposer holds state for composing tenant resources. -type tenantComposer struct { +// composer holds state for composing resources from an XR. +type composer struct { oxr *resource.Composite - name string desired map[resource.Name]any } -func newTenantComposer(oxr *resource.Composite) (*tenantComposer, error) { - name, err := oxr.Resource.GetString("spec.name") - if err != nil { - return nil, errors.Wrap(err, "spec.name is required") - } - return &tenantComposer{ +func newComposer(oxr *resource.Composite) *composer { + return &composer{ oxr: oxr, - name: name, desired: make(map[resource.Name]any), - }, nil + } } -func (tc *tenantComposer) getString(path string) string { - val, _ := tc.oxr.Resource.GetString(path) +func (c *composer) getString(path string) string { + val, _ := c.oxr.Resource.GetString(path) return val } -func (tc *tenantComposer) getStringArray(path string) []string { - val, _ := tc.oxr.Resource.GetStringArray(path) +func (c *composer) getStringArray(path string) []string { + val, _ := c.oxr.Resource.GetStringArray(path) return val } -func (tc *tenantComposer) featureEnabled(feature string) bool { - enabled, err := tc.oxr.Resource.GetBool("spec.features." + feature + ".enabled") +func (c *composer) featureEnabled(feature string) bool { + enabled, err := c.oxr.Resource.GetBool("spec.features." + feature + ".enabled") return err == nil && enabled } -func (tc *tenantComposer) add(name resource.Name, obj any) { - tc.desired[name] = obj +func (c *composer) add(name resource.Name, obj any) { + c.desired[name] = obj } -func (tc *tenantComposer) composeBase() error { - tc.add("namespace", resources.NewTenantNamespace(tc.name)) +func (c *composer) name() string { + return c.getString("spec.name") +} + +func (c *composer) composeBase() error { + name := c.name() + if name == "" { + return errors.New("spec.name is required") + } - project, err := resources.NewTenantProject(tc.name, tc.getString("spec.description"), tc.getStringArray("spec.sourceRepos")) + c.add("namespace", resources.NewTenantNamespace(name)) + + project, err := resources.NewTenantProject(name, c.getString("spec.description"), c.getStringArray("spec.sourceRepos")) if err != nil { return errors.Wrap(err, "spec.sourceRepos is required") } - tc.add("project", project) + c.add("project", project) return nil } -func (tc *tenantComposer) composeArgoWorkflows() { - if !tc.featureEnabled("argoWorkflows") { +func (c *composer) composeArgoWorkflows() { + if !c.featureEnabled("argoWorkflows") { return } - tc.add("argo-workflow-sa", resources.NewServiceAccount(resources.ServiceAccountConfig{ + name := c.name() + c.add("argo-workflow-sa", resources.NewServiceAccount(resources.ServiceAccountConfig{ Name: "argo-workflow", - Namespace: tc.name, + Namespace: name, })) - tc.add("argo-workflow-role", resources.NewRole(resources.RoleConfig{ + c.add("argo-workflow-role", resources.NewRole(resources.RoleConfig{ Name: "argo-workflow", - Namespace: tc.name, + Namespace: name, Rules: []rbacv1.PolicyRule{{ APIGroups: []string{"argoproj.io"}, Resources: []string{"workflowtaskresults"}, Verbs: []string{"create", "patch"}, }}, })) - tc.add("argo-workflow-rolebinding", resources.NewRoleBinding(resources.RoleBindingConfig{ + c.add("argo-workflow-rolebinding", resources.NewRoleBinding(resources.RoleBindingConfig{ Name: "argo-workflow", - Namespace: tc.name, + Namespace: name, RoleName: "argo-workflow", ServiceAccountName: "argo-workflow", })) @@ -111,18 +115,14 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) "xr-kind", oxr.Resource.GetKind(), ) - tc, err := newTenantComposer(oxr) - if err != nil { - response.Fatal(rsp, err) - return rsp, nil - } + c := newComposer(oxr) - if err := tc.composeBase(); err != nil { + if err := c.composeBase(); err != nil { response.Fatal(rsp, err) return rsp, nil } - tc.composeArgoWorkflows() + c.composeArgoWorkflows() desired, err := request.GetDesiredComposedResources(req) if err != nil { @@ -130,13 +130,13 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) return rsp, nil } - for name, obj := range tc.desired { - c := composed.New() - if err := resources.ConvertViaJSON(c, obj); err != nil { + for name, obj := range c.desired { + res := composed.New() + if err := resources.ConvertViaJSON(res, obj); err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot convert %s to unstructured", name)) return rsp, nil } - desired[name] = &resource.DesiredComposed{Resource: c} + desired[name] = &resource.DesiredComposed{Resource: res} } if err := response.SetDesiredComposedResources(rsp, desired); err != nil { @@ -144,7 +144,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) return rsp, nil } - log.Info("Composed tenant resources", "tenant", tc.name) + log.Info("Composed tenant resources", "tenant", c.name()) response.ConditionTrue(rsp, "FunctionSuccess", "Success").TargetCompositeAndClaim() return rsp, nil From b768a99a0011e1a5529dec4620a42fbb7ae80144 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 21:20:16 +0000 Subject: [PATCH 6/9] refactor: extract composer package from xtenant --- apis/management/functions/xtenant/fn.go | 80 +++----- apis/management/functions/xtenant/go.mod | 5 +- apis/pkg/composer/composer.go | 95 ++++++++++ apis/pkg/composer/composer_test.go | 103 ++++++++++ apis/pkg/composer/go.mod | 72 +++++++ apis/pkg/composer/go.sum | 231 +++++++++++++++++++++++ 6 files changed, 528 insertions(+), 58 deletions(-) create mode 100644 apis/pkg/composer/composer.go create mode 100644 apis/pkg/composer/composer_test.go create mode 100644 apis/pkg/composer/go.mod create mode 100644 apis/pkg/composer/go.sum diff --git a/apis/management/functions/xtenant/fn.go b/apis/management/functions/xtenant/fn.go index 5f0ae88..09aa6d9 100644 --- a/apis/management/functions/xtenant/fn.go +++ b/apis/management/functions/xtenant/fn.go @@ -3,6 +3,7 @@ package main import ( "context" + "github.com/mdaops/cortex/configurations/pkg/composer" "github.com/mdaops/cortex/configurations/pkg/resources" rbacv1 "k8s.io/api/rbac/v1" @@ -15,75 +16,41 @@ import ( "github.com/crossplane/function-sdk-go/response" ) -// Function implements the Crossplane composition function for Tenant resources. type Function struct { fnv1.UnimplementedFunctionRunnerServiceServer - log logging.Logger } -// composer holds state for composing resources from an XR. -type composer struct { - oxr *resource.Composite - desired map[resource.Name]any -} - -func newComposer(oxr *resource.Composite) *composer { - return &composer{ - oxr: oxr, - desired: make(map[resource.Name]any), +func composeBase(c *composer.Composer) error { + name := c.GetString("spec.name") + if err := c.Err(); err != nil { + return err } -} -func (c *composer) getString(path string) string { - val, _ := c.oxr.Resource.GetString(path) - return val -} - -func (c *composer) getStringArray(path string) []string { - val, _ := c.oxr.Resource.GetStringArray(path) - return val -} - -func (c *composer) featureEnabled(feature string) bool { - enabled, err := c.oxr.Resource.GetBool("spec.features." + feature + ".enabled") - return err == nil && enabled -} + c.Add("namespace", resources.NewTenantNamespace(name)) -func (c *composer) add(name resource.Name, obj any) { - c.desired[name] = obj -} - -func (c *composer) name() string { - return c.getString("spec.name") -} - -func (c *composer) composeBase() error { - name := c.name() - if name == "" { - return errors.New("spec.name is required") - } - - c.add("namespace", resources.NewTenantNamespace(name)) - - project, err := resources.NewTenantProject(name, c.getString("spec.description"), c.getStringArray("spec.sourceRepos")) + project, err := resources.NewTenantProject(name, c.GetString("spec.description"), c.GetStringArray("spec.sourceRepos")) + c.ClearErrs() if err != nil { return errors.Wrap(err, "spec.sourceRepos is required") } - c.add("project", project) + c.Add("project", project) return nil } -func (c *composer) composeArgoWorkflows() { - if !c.featureEnabled("argoWorkflows") { +func composeArgoWorkflows(c *composer.Composer) { + if !c.GetBool("spec.features.argoWorkflows.enabled") { + c.ClearErrs() return } - name := c.name() - c.add("argo-workflow-sa", resources.NewServiceAccount(resources.ServiceAccountConfig{ + name := c.GetString("spec.name") + c.ClearErrs() + + c.Add("argo-workflow-sa", resources.NewServiceAccount(resources.ServiceAccountConfig{ Name: "argo-workflow", Namespace: name, })) - c.add("argo-workflow-role", resources.NewRole(resources.RoleConfig{ + c.Add("argo-workflow-role", resources.NewRole(resources.RoleConfig{ Name: "argo-workflow", Namespace: name, Rules: []rbacv1.PolicyRule{{ @@ -92,7 +59,7 @@ func (c *composer) composeArgoWorkflows() { Verbs: []string{"create", "patch"}, }}, })) - c.add("argo-workflow-rolebinding", resources.NewRoleBinding(resources.RoleBindingConfig{ + c.Add("argo-workflow-rolebinding", resources.NewRoleBinding(resources.RoleBindingConfig{ Name: "argo-workflow", Namespace: name, RoleName: "argo-workflow", @@ -100,7 +67,6 @@ func (c *composer) composeArgoWorkflows() { })) } -// RunFunction composes namespace and ArgoCD project resources for a Tenant. func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) { rsp := response.To(req, response.DefaultTTL) @@ -115,14 +81,14 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) "xr-kind", oxr.Resource.GetKind(), ) - c := newComposer(oxr) + c := composer.New(oxr) - if err := c.composeBase(); err != nil { + if err := composeBase(c); err != nil { response.Fatal(rsp, err) return rsp, nil } - c.composeArgoWorkflows() + composeArgoWorkflows(c) desired, err := request.GetDesiredComposedResources(req) if err != nil { @@ -130,7 +96,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) return rsp, nil } - for name, obj := range c.desired { + for name, obj := range c.Desired() { res := composed.New() if err := resources.ConvertViaJSON(res, obj); err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot convert %s to unstructured", name)) @@ -144,7 +110,7 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) return rsp, nil } - log.Info("Composed tenant resources", "tenant", c.name()) + log.Info("Composed tenant resources", "tenant", c.GetString("spec.name")) response.ConditionTrue(rsp, "FunctionSuccess", "Success").TargetCompositeAndClaim() return rsp, nil diff --git a/apis/management/functions/xtenant/go.mod b/apis/management/functions/xtenant/go.mod index 91e5b80..4c77896 100644 --- a/apis/management/functions/xtenant/go.mod +++ b/apis/management/functions/xtenant/go.mod @@ -8,10 +8,14 @@ require ( github.com/alecthomas/kong v1.13.0 github.com/crossplane/function-sdk-go v0.5.0 github.com/google/go-cmp v0.7.0 + github.com/mdaops/cortex/configurations/pkg/composer v0.0.0 github.com/mdaops/cortex/configurations/pkg/resources v0.0.0 google.golang.org/protobuf v1.36.11 + k8s.io/api v0.34.0 ) +replace github.com/mdaops/cortex/configurations/pkg/composer => ../../../pkg/composer + replace github.com/mdaops/cortex/configurations/pkg/resources => ../../../pkg/resources require ( @@ -74,7 +78,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.0 // indirect k8s.io/apiextensions-apiserver v0.34.0 // indirect k8s.io/apimachinery v0.34.3 // indirect k8s.io/client-go v0.34.0 // indirect diff --git a/apis/pkg/composer/composer.go b/apis/pkg/composer/composer.go new file mode 100644 index 0000000..2880bc0 --- /dev/null +++ b/apis/pkg/composer/composer.go @@ -0,0 +1,95 @@ +package composer + +import ( + "errors" + + "github.com/crossplane/function-sdk-go/resource" +) + +var ErrPathNotFound = errors.New("path not found") + +// PathError wraps an error with the path that caused it. +type PathError struct { + Path string + Err error +} + +func (e *PathError) Error() string { + return "path " + e.Path + ": " + e.Err.Error() +} + +func (e *PathError) Unwrap() error { + return e.Err +} + +func (e *PathError) Is(target error) bool { + return target == ErrPathNotFound +} + +// Composer accumulates desired resources and errors from an XR. +type Composer struct { + oxr *resource.Composite + desired map[resource.Name]any + errs []error +} + +// New creates a Composer for the given observed composite resource. +func New(oxr *resource.Composite) *Composer { + return &Composer{ + oxr: oxr, + desired: make(map[resource.Name]any), + } +} + +// GetString returns the string at path, recording an error if not found. +func (c *Composer) GetString(path string) string { + val, err := c.oxr.Resource.GetString(path) + if err != nil { + c.errs = append(c.errs, &PathError{Path: path, Err: err}) + return "" + } + return val +} + +// GetStringArray returns the string array at path, recording an error if not found. +func (c *Composer) GetStringArray(path string) []string { + val, err := c.oxr.Resource.GetStringArray(path) + if err != nil { + c.errs = append(c.errs, &PathError{Path: path, Err: err}) + return nil + } + return val +} + +// GetBool returns the bool at path, recording an error if not found. +func (c *Composer) GetBool(path string) bool { + val, err := c.oxr.Resource.GetBool(path) + if err != nil { + c.errs = append(c.errs, &PathError{Path: path, Err: err}) + return false + } + return val +} + +// Add adds a resource to the desired set. +func (c *Composer) Add(name resource.Name, obj any) { + c.desired[name] = obj +} + +// Desired returns the accumulated desired resources. +func (c *Composer) Desired() map[resource.Name]any { + return c.desired +} + +// Err returns accumulated errors, or nil if none. +func (c *Composer) Err() error { + if len(c.errs) == 0 { + return nil + } + return errors.Join(c.errs...) +} + +// ClearErrs clears accumulated errors. +func (c *Composer) ClearErrs() { + c.errs = nil +} diff --git a/apis/pkg/composer/composer_test.go b/apis/pkg/composer/composer_test.go new file mode 100644 index 0000000..df0d700 --- /dev/null +++ b/apis/pkg/composer/composer_test.go @@ -0,0 +1,103 @@ +package composer + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/crossplane/function-sdk-go/resource" + "github.com/crossplane/function-sdk-go/resource/composite" +) + +func makeComposite(jsonStr string) *resource.Composite { + xr := composite.New() + var data map[string]any + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + panic(err) + } + xr.SetUnstructuredContent(data) + return &resource.Composite{Resource: xr} +} + +func TestComposer_GetString(t *testing.T) { + c := New(makeComposite(`{ + "apiVersion": "test/v1", + "kind": "Test", + "spec": {"name": "test-value"} + }`)) + + val := c.GetString("spec.name") + if val != "test-value" { + t.Errorf("expected test-value, got %s", val) + } + if c.Err() != nil { + t.Errorf("expected no error, got %v", c.Err()) + } +} + +func TestComposer_GetString_NotFound(t *testing.T) { + c := New(makeComposite(`{ + "apiVersion": "test/v1", + "kind": "Test", + "spec": {} + }`)) + + val := c.GetString("spec.missing") + if val != "" { + t.Errorf("expected empty string, got %s", val) + } + if c.Err() == nil { + t.Error("expected error") + } + if !errors.Is(c.Err(), ErrPathNotFound) { + t.Errorf("expected ErrPathNotFound, got %v", c.Err()) + } +} + +func TestComposer_GetBool(t *testing.T) { + c := New(makeComposite(`{ + "apiVersion": "test/v1", + "kind": "Test", + "spec": {"enabled": true} + }`)) + + val := c.GetBool("spec.enabled") + if !val { + t.Error("expected true") + } + if c.Err() != nil { + t.Errorf("expected no error, got %v", c.Err()) + } +} + +func TestComposer_Add(t *testing.T) { + c := New(makeComposite(`{ + "apiVersion": "test/v1", + "kind": "Test" + }`)) + + c.Add("resource-a", "value-a") + c.Add("resource-b", "value-b") + + if len(c.Desired()) != 2 { + t.Errorf("expected 2 desired, got %d", len(c.Desired())) + } +} + +func TestComposer_ClearErrs(t *testing.T) { + c := New(makeComposite(`{ + "apiVersion": "test/v1", + "kind": "Test", + "spec": {} + }`)) + + c.GetString("spec.missing") + if c.Err() == nil { + t.Error("expected error") + } + + c.ClearErrs() + if c.Err() != nil { + t.Error("expected no error after clear") + } +} diff --git a/apis/pkg/composer/go.mod b/apis/pkg/composer/go.mod new file mode 100644 index 0000000..8da6f1a --- /dev/null +++ b/apis/pkg/composer/go.mod @@ -0,0 +1,72 @@ +module github.com/mdaops/cortex/configurations/pkg/composer + +go 1.24.11 + +toolchain go1.24.12 + +require github.com/crossplane/function-sdk-go v0.5.0 + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/crossplane/crossplane-runtime/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.38.0 // indirect + golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.33.0 // indirect + k8s.io/apiextensions-apiserver v0.33.0 // indirect + k8s.io/apimachinery v0.33.0 // indirect + k8s.io/client-go v0.33.0 // indirect + k8s.io/code-generator v0.33.0 // indirect + k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/controller-runtime v0.19.0 // indirect + sigs.k8s.io/controller-tools v0.18.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/apis/pkg/composer/go.sum b/apis/pkg/composer/go.sum new file mode 100644 index 0000000..536ac89 --- /dev/null +++ b/apis/pkg/composer/go.sum @@ -0,0 +1,231 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crossplane/crossplane-runtime/v2 v2.0.0 h1:PK2pTKfshdDZ5IfoiMRiCi0PBnIjqbS0KGXEJgRdrb4= +github.com/crossplane/crossplane-runtime/v2 v2.0.0/go.mod h1:pkd5UzmE8esaZAApevMutR832GjJ1Qgc5Ngr78ByxrI= +github.com/crossplane/function-sdk-go v0.5.0 h1:wF+pOsR6jlIUHZjpSL6tbuSP0UB7s25+4AGkNytsHKk= +github.com/crossplane/function-sdk-go v0.5.0/go.mod h1:bIvGe17dIdpZ/YULrg5xAP8MK+eS3ot5BAuQEntaeWc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1 h1:xcuWappghOVI8iNWoF2OKahVejd1LSVi/v4JED44Amo= +github.com/go-json-experiment/json v0.0.0-20240815175050-ebd3a8989ca1/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= +k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/code-generator v0.33.0 h1:B212FVl6EFqNmlgdOZYWNi77yBv+ed3QgQsMR8YQCw4= +k8s.io/code-generator v0.33.0/go.mod h1:KnJRokGxjvbBQkSJkbVuBbu6z4B0rC7ynkpY5Aw6m9o= +k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= +k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 h1:gAXU86Fmbr/ktY17lkHwSjw5aoThQvhnstGGIYKlKYc= +k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911/go.mod h1:GLOk5B+hDbRROvt0X2+hqX64v/zO3vXN7J78OUmBSKw= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= +sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/controller-tools v0.18.0 h1:rGxGZCZTV2wJreeRgqVoWab/mfcumTMmSwKzoM9xrsE= +sigs.k8s.io/controller-tools v0.18.0/go.mod h1:gLKoiGBriyNh+x1rWtUQnakUYEujErjXs9pf+x/8n1U= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= From 697bfbfd8bbbefbfa2bdf0f45bf610b30be105d4 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 21:21:31 +0000 Subject: [PATCH 7/9] fix: add doc comments for exported symbols --- apis/management/functions/xtenant/fn.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apis/management/functions/xtenant/fn.go b/apis/management/functions/xtenant/fn.go index 09aa6d9..088a6e9 100644 --- a/apis/management/functions/xtenant/fn.go +++ b/apis/management/functions/xtenant/fn.go @@ -16,6 +16,7 @@ import ( "github.com/crossplane/function-sdk-go/response" ) +// Function implements the Crossplane composition function for Tenant resources. type Function struct { fnv1.UnimplementedFunctionRunnerServiceServer log logging.Logger @@ -67,6 +68,7 @@ func composeArgoWorkflows(c *composer.Composer) { })) } +// RunFunction composes namespace and ArgoCD project resources for a Tenant. func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) { rsp := response.To(req, response.DefaultTTL) From 11af51d4fa21b67ec7d31e54446253257684427f Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 21:22:59 +0000 Subject: [PATCH 8/9] fix: copy composer pkg in dockerfile --- apis/management/functions/xtenant/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/apis/management/functions/xtenant/Dockerfile b/apis/management/functions/xtenant/Dockerfile index 8d2a880..e39c2e2 100644 --- a/apis/management/functions/xtenant/Dockerfile +++ b/apis/management/functions/xtenant/Dockerfile @@ -7,6 +7,7 @@ FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS build WORKDIR /src ENV CGO_ENABLED=0 +COPY apis/pkg/composer/ /src/apis/pkg/composer/ COPY apis/pkg/resources/ /src/apis/pkg/resources/ COPY apis/management/functions/xtenant/ /src/apis/management/functions/xtenant/ From 66ba9d15fef17c6c7cc69083b63ee91a3a48ff33 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 18 Jan 2026 21:24:49 +0000 Subject: [PATCH 9/9] refactor: use go.work for multi-module builds --- apis/go.work | 7 +++++++ apis/management/functions/xtenant/Dockerfile | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 apis/go.work diff --git a/apis/go.work b/apis/go.work new file mode 100644 index 0000000..7d74d7d --- /dev/null +++ b/apis/go.work @@ -0,0 +1,7 @@ +go 1.24.11 + +use ( + ./management/functions/xtenant + ./pkg/composer + ./pkg/resources +) diff --git a/apis/management/functions/xtenant/Dockerfile b/apis/management/functions/xtenant/Dockerfile index e39c2e2..6ec5c63 100644 --- a/apis/management/functions/xtenant/Dockerfile +++ b/apis/management/functions/xtenant/Dockerfile @@ -4,12 +4,12 @@ ARG GO_VERSION=1 FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS build -WORKDIR /src +WORKDIR /src/apis ENV CGO_ENABLED=0 -COPY apis/pkg/composer/ /src/apis/pkg/composer/ -COPY apis/pkg/resources/ /src/apis/pkg/resources/ -COPY apis/management/functions/xtenant/ /src/apis/management/functions/xtenant/ +COPY apis/go.work ./ +COPY apis/pkg/ ./pkg/ +COPY apis/management/functions/xtenant/ ./management/functions/xtenant/ WORKDIR /src/apis/management/functions/xtenant