From b2be2668e9703d3df2aedb0b5ce6f533b82e3cf2 Mon Sep 17 00:00:00 2001 From: Lency Qian Date: Thu, 6 Jun 2024 11:41:46 +0800 Subject: [PATCH 1/6] update materialize and webhook --- .../managers/campaigns/campaigns-manager.go | 54 +++++- .../managers/catalogs/catalogs-manager.go | 45 +++++ .../managers/instances/instances-manager.go | 53 ++++++ .../managers/solutions/solutions-manager.go | 54 +++++- .../managers/targets/targets-manager.go | 48 ++++++ .../stage/materialize/materialize.go | 85 ++++++++-- .../stage/materialize/materialize_test.go | 18 +- .../k8s/hello-world/instance-container.yaml | 5 + docs/samples/k8s/hello-world/instance.yaml | 6 +- .../k8s/hello-world/solution-container.yaml | 5 + docs/samples/k8s/hello-world/solution.yaml | 5 +- .../k8s/hello-world/target-container.yaml | 5 + docs/samples/k8s/hello-world/target.yaml | 5 +- docs/samples/multisite/activation.yaml | 2 +- docs/samples/multisite/campaign.yaml | 3 +- docs/samples/multisite/catalog-catalog.yaml | 5 +- docs/samples/multisite/instance-catalog.yaml | 7 +- docs/samples/multisite/solution-catalog.yaml | 11 +- docs/samples/multisite/target-catalog.yaml | 7 +- k8s/apis/fabric/v1/target_webhook.go | 44 +++++ k8s/apis/federation/v1/catalog_webhook.go | 88 ++++++++-- k8s/apis/solution/v1/instance_webhook.go | 48 +++++- k8s/apis/solution/v1/solution_webhook.go | 80 ++++++++- k8s/apis/workflow/v1/campaign_webhook.go | 160 ++++++++++++++++++ k8s/config/oss/webhook/manifests.yaml | 40 +++++ k8s/configutils/configutil.go | 44 +++++ k8s/main.go | 4 + .../templates/symphony-core/symphonyk8s.yaml | 40 +++++ .../lib/testhelpers/manifestbuilder.go | 58 +++++++ test/integration/lib/testhelpers/types.go | 61 +++++-- .../oss/instance-container.yaml | 5 + .../manifestTemplates/oss/instance.yaml | 3 +- .../oss/solution-container.yaml | 5 + .../manifestTemplates/oss/solution.yaml | 3 +- .../oss/target-container.yaml | 5 + .../manifestTemplates/oss/target.yaml | 1 + .../01.update/verify/manifest_test.go | 12 ++ .../scenarios/02.basic/magefile.go | 6 + .../manifest/oss/instance-container.yaml | 5 + .../02.basic/manifest/oss/instance.yaml | 1 + .../manifest/oss/solution-container.yaml | 5 + .../02.basic/manifest/oss/solution.yaml | 1 + .../manifest/oss/target-container.yaml | 5 + .../02.basic/manifest/oss/target.yaml | 1 + .../03.basicWithNsDelete/magefile.go | 3 + .../manifest/oss/instance-container.yaml | 5 + .../manifest/oss/instance.yaml | 1 + .../manifest/oss/solution-container.yaml | 5 + .../manifest/oss/solution.yaml | 3 +- .../manifest/oss/target-container.yaml | 5 + .../manifest/oss/target.yaml | 1 + .../scenarios/04.workflow/magefile.go | 6 + .../manifest/campaign-container.yaml | 5 + .../04.workflow/manifest/campaign.yaml | 10 +- .../manifest/catalog-catalog-container.yaml | 5 + .../04.workflow/manifest/catalog-catalog.yaml | 5 +- .../manifest/instance-catalog-container.yaml | 5 + .../manifest/instance-catalog.yaml | 7 +- .../manifest/solution-catalog-container.yaml | 5 + .../manifest/solution-catalog.yaml | 9 +- .../manifest/target-catalog-container.yaml | 5 + .../04.workflow/manifest/target-catalog.yaml | 7 +- .../05.catalog/catalogs/asset-container.yaml | 5 + .../scenarios/05.catalog/catalogs/asset.yaml | 1 + .../05.catalog/catalogs/config-container.yaml | 5 + .../scenarios/05.catalog/catalogs/config.yaml | 1 + .../catalogs/instance-container.yaml | 5 + .../05.catalog/catalogs/instance.yaml | 3 + .../05.catalog/catalogs/schema-container.yaml | 5 + .../scenarios/05.catalog/catalogs/schema.yaml | 1 + .../catalogs/solution-container.yaml | 5 + .../05.catalog/catalogs/solution.yaml | 3 + .../05.catalog/catalogs/target-container.yaml | 5 + .../scenarios/05.catalog/catalogs/target.yaml | 3 + .../scenarios/05.catalog/magefile.go | 7 + .../06.ado/create_update_fallback_test.go | 23 ++- .../scenarios/06.ado/create_update_test.go | 24 ++- .../scenarios/06.ado/delete_test.go | 24 ++- .../06.ado/manifest/instance-container.yaml | 5 + .../scenarios/06.ado/manifest/instance.yaml | 1 + .../06.ado/manifest/solution-container.yaml | 5 + .../scenarios/06.ado/manifest/solution.yaml | 1 + .../06.ado/manifest/target-container.yaml | 5 + .../scenarios/06.ado/manifest/target.yaml | 1 + .../integration/scenarios/06.ado/rbac_test.go | 24 ++- .../scenarios/06.ado/suite_test.go | 9 + 86 files changed, 1310 insertions(+), 101 deletions(-) create mode 100644 docs/samples/k8s/hello-world/instance-container.yaml create mode 100644 docs/samples/k8s/hello-world/solution-container.yaml create mode 100644 docs/samples/k8s/hello-world/target-container.yaml create mode 100644 k8s/apis/workflow/v1/campaign_webhook.go create mode 100644 test/integration/scenarios/01.update/manifestTemplates/oss/instance-container.yaml create mode 100644 test/integration/scenarios/01.update/manifestTemplates/oss/solution-container.yaml create mode 100644 test/integration/scenarios/01.update/manifestTemplates/oss/target-container.yaml create mode 100644 test/integration/scenarios/02.basic/manifest/oss/instance-container.yaml create mode 100644 test/integration/scenarios/02.basic/manifest/oss/solution-container.yaml create mode 100644 test/integration/scenarios/02.basic/manifest/oss/target-container.yaml create mode 100644 test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-container.yaml create mode 100644 test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution-container.yaml create mode 100644 test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-container.yaml create mode 100644 test/integration/scenarios/04.workflow/manifest/campaign-container.yaml create mode 100644 test/integration/scenarios/04.workflow/manifest/catalog-catalog-container.yaml create mode 100644 test/integration/scenarios/04.workflow/manifest/instance-catalog-container.yaml create mode 100644 test/integration/scenarios/04.workflow/manifest/solution-catalog-container.yaml create mode 100644 test/integration/scenarios/04.workflow/manifest/target-catalog-container.yaml create mode 100644 test/integration/scenarios/05.catalog/catalogs/asset-container.yaml create mode 100644 test/integration/scenarios/05.catalog/catalogs/config-container.yaml create mode 100644 test/integration/scenarios/05.catalog/catalogs/instance-container.yaml create mode 100644 test/integration/scenarios/05.catalog/catalogs/schema-container.yaml create mode 100644 test/integration/scenarios/05.catalog/catalogs/solution-container.yaml create mode 100644 test/integration/scenarios/05.catalog/catalogs/target-container.yaml create mode 100644 test/integration/scenarios/06.ado/manifest/instance-container.yaml create mode 100644 test/integration/scenarios/06.ado/manifest/solution-container.yaml create mode 100644 test/integration/scenarios/06.ado/manifest/target-container.yaml diff --git a/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go b/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go index a47c7f6af..f1452649c 100644 --- a/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go +++ b/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go @@ -19,8 +19,11 @@ import ( observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) +var log = logger.NewLogger("coa.runtime") + type CampaignsManager struct { managers.Manager StateProvider states.IStateProvider @@ -96,6 +99,51 @@ func (m *CampaignsManager) UpsertState(ctx context.Context, name string, state m } state.ObjectMeta.FixNames(name) + if state.Spec != nil { + rootResource := state.Spec.RootResource + if rootResource != "" { + log.Debugf(" M (Campaigns): campaign root resource: %s, campaign: %s", rootResource, name) + resourceName := "campaigncontainers" + kind := "CampaignContainer" + containerMetadata := map[string]interface{}{ + "version": "v1", + "group": model.WorkflowGroup, + "resource": resourceName, + "namespace": state.ObjectMeta.Namespace, + "kind": kind, + } + getRequest := states.GetRequest{ + ID: rootResource, + Metadata: containerMetadata, + } + _, err = m.StateProvider.Get(ctx, getRequest) + if err != nil { + log.Debugf(" M (Campaigns): get campaign container %s, err %v", rootResource, err) + cErr, ok := err.(v1alpha2.COAError) + if ok && cErr.State == v1alpha2.NotFound { + containerBody := map[string]interface{}{ + "apiVersion": model.WorkflowGroup + "/v1", + "kind": kind, + "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, + "spec": model.CampaignContainerSpec{}, + } + containerUpsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: rootResource, + Body: containerBody, + }, + Metadata: containerMetadata, + } + _, err = m.StateProvider.Upsert(ctx, containerUpsertRequest) + if err != nil { + log.Errorf(" M (Campaigns): failed to create campaign container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) + return err + } + } + } + } + } + upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ ID: name, @@ -155,13 +203,13 @@ func (t *CampaignsManager) ListState(ctx context.Context, namespace string) ([]m "kind": "Campaign", }, } - var solutions []states.StateEntry - solutions, _, err = t.StateProvider.List(ctx, listRequest) + var campaigns []states.StateEntry + campaigns, _, err = t.StateProvider.List(ctx, listRequest) if err != nil { return nil, err } ret := make([]model.CampaignState, 0) - for _, t := range solutions { + for _, t := range campaigns { var rt model.CampaignState rt, err = getCampaignState(t.Body) if err != nil { diff --git a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go index 6fe1a8c4b..53677d2fc 100644 --- a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go +++ b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go @@ -152,6 +152,51 @@ func (m *CatalogsManager) UpsertState(ctx context.Context, name string, state mo return err } + if state.Spec != nil { + rootResource := state.Spec.RootResource + if rootResource != "" { + log.Debugf(" M (Catalogs): catalog root resource: %s, catalog: %s", rootResource, name) + resourceName := "catalogcontainers" + kind := "CatalogContainer" + containerMetadata := map[string]interface{}{ + "version": "v1", + "group": model.FederationGroup, + "resource": resourceName, + "namespace": state.ObjectMeta.Namespace, + "kind": kind, + } + getRequest := states.GetRequest{ + ID: rootResource, + Metadata: containerMetadata, + } + _, err = m.StateProvider.Get(ctx, getRequest) + if err != nil { + log.Debugf(" M (Catalogs): get catalog container %s, err %v", rootResource, err) + cErr, ok := err.(v1alpha2.COAError) + if ok && cErr.State == v1alpha2.NotFound { + containerBody := map[string]interface{}{ + "apiVersion": model.FederationGroup + "/v1", + "kind": kind, + "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, + "spec": model.CatalogContainerSpec{}, + } + containerUpsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: rootResource, + Body: containerBody, + }, + Metadata: containerMetadata, + } + _, err = m.StateProvider.Upsert(ctx, containerUpsertRequest) + if err != nil { + log.Errorf(" M (Catalogs): failed to create catalog container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) + return err + } + } + } + } + } + upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ ID: name, diff --git a/api/pkg/apis/v1alpha1/managers/instances/instances-manager.go b/api/pkg/apis/v1alpha1/managers/instances/instances-manager.go index 9d3c2de37..f3261708e 100644 --- a/api/pkg/apis/v1alpha1/managers/instances/instances-manager.go +++ b/api/pkg/apis/v1alpha1/managers/instances/instances-manager.go @@ -17,11 +17,14 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" ) +var log = logger.NewLogger("coa.runtime") + type InstancesManager struct { managers.Manager StateProvider states.IStateProvider @@ -80,8 +83,58 @@ func (t *InstancesManager) UpsertState(ctx context.Context, name string, state m "spec": state.Spec, } generation := "" + if state.Spec != nil { generation = state.Spec.Generation + rootResource := state.Spec.RootResource + if rootResource != "" { + log.Debugf(" M (Instances): instance root resource: %s, instance: %s", rootResource, name) + resourceName := "instancecontainers" + kind := "InstanceContainer" + containerMetadata := map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": resourceName, + "namespace": state.ObjectMeta.Namespace, + "kind": kind, + } + getRequest := states.GetRequest{ + ID: rootResource, + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": resourceName, + "namespace": state.ObjectMeta.Namespace, + "kind": kind, + }, + } + _, err = t.StateProvider.Get(ctx, getRequest) + if err != nil { + coae := err.(v1alpha2.COAError) + log.Infof(" M (Instances): get instance container %s, err %v, namespace: %v", rootResource, err, coae.State) + cErr, ok := err.(v1alpha2.COAError) + if ok && cErr.State == v1alpha2.NotFound { + containerBody := map[string]interface{}{ + "apiVersion": model.SolutionGroup + "/v1", + "kind": kind, + "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, + "spec": model.InstanceContainerSpec{}, + } + containerUpsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: rootResource, + Body: containerBody, + }, + Metadata: containerMetadata, + } + _, err = t.StateProvider.Upsert(ctx, containerUpsertRequest) + if err != nil { + log.Errorf(" M (Instances): failed to create instance container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) + return err + } + } + } + } } upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ diff --git a/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go b/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go index 11b484e75..af491d66e 100644 --- a/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go +++ b/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go @@ -15,13 +15,15 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" - "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" - observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) +var log = logger.NewLogger("coa.runtime") + type SolutionsManager struct { managers.Manager StateProvider states.IStateProvider @@ -79,6 +81,52 @@ func (t *SolutionsManager) UpsertState(ctx context.Context, name string, state m "metadata": state.ObjectMeta, "spec": state.Spec, } + + if state.Spec != nil { + rootResource := state.Spec.RootResource + if rootResource != "" { + log.Debugf(" M (Solutions): solution root resource: %s, solution: %s", rootResource, name) + resourceName := "solutioncontainers" + kind := "SolutionContainer" + containerMetadata := map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": resourceName, + "namespace": state.ObjectMeta.Namespace, + "kind": kind, + } + getRequest := states.GetRequest{ + ID: rootResource, + Metadata: containerMetadata, + } + _, err = t.StateProvider.Get(ctx, getRequest) + if err != nil { + log.Debugf(" M (Solutions): get solution container %s, err %v", rootResource, err) + cErr, ok := err.(v1alpha2.COAError) + if ok && cErr.State == v1alpha2.NotFound { + containerBody := map[string]interface{}{ + "apiVersion": model.SolutionGroup + "/v1", + "kind": kind, + "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, + "spec": model.SolutionContainerSpec{}, + } + containerUpsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: rootResource, + Body: containerBody, + }, + Metadata: containerMetadata, + } + _, err = t.StateProvider.Upsert(ctx, containerUpsertRequest) + if err != nil { + log.Errorf(" M (Solutions): failed to create solution container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) + return err + } + } + } + } + } + upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ ID: name, diff --git a/api/pkg/apis/v1alpha1/managers/targets/targets-manager.go b/api/pkg/apis/v1alpha1/managers/targets/targets-manager.go index 5b50b7318..126fcfaf8 100644 --- a/api/pkg/apis/v1alpha1/managers/targets/targets-manager.go +++ b/api/pkg/apis/v1alpha1/managers/targets/targets-manager.go @@ -19,10 +19,13 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/registry" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" ) +var log = logger.NewLogger("coa.runtime") + type TargetsManager struct { managers.Manager StateProvider states.IStateProvider @@ -83,6 +86,51 @@ func (t *TargetsManager) UpsertState(ctx context.Context, name string, state mod "spec": state.Spec, } + if state.Spec != nil { + rootResource := state.Spec.RootResource + if rootResource != "" { + log.Debugf(" M (Targets): target root resource: %s, target: %s", rootResource, name) + resourceName := "targetcontainers" + kind := "TargetContainer" + containerMetadata := map[string]interface{}{ + "version": "v1", + "group": model.FabricGroup, + "resource": resourceName, + "namespace": state.ObjectMeta.Namespace, + "kind": kind, + } + getRequest := states.GetRequest{ + ID: rootResource, + Metadata: containerMetadata, + } + _, err = t.StateProvider.Get(ctx, getRequest) + if err != nil { + log.Debugf(" M (Targets): get target container %s, err %v", rootResource, err) + cErr, ok := err.(v1alpha2.COAError) + if ok && cErr.State == v1alpha2.NotFound { + containerBody := map[string]interface{}{ + "apiVersion": model.FabricGroup + "/v1", + "kind": kind, + "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, + "spec": model.TargetContainerSpec{}, + } + containerUpsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: rootResource, + Body: containerBody, + }, + Metadata: containerMetadata, + } + _, err = t.StateProvider.Upsert(ctx, containerUpsertRequest) + if err != nil { + log.Errorf(" M (Targets): failed to create target container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) + return err + } + } + } + } + } + upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ ID: name, diff --git a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go index 60f33f7e6..352782a23 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go +++ b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go @@ -159,11 +159,23 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to unmarshal instance state for catalog %s: %s", name, err.Error()) return outputs, false, err } - // If inner instace defines a display name, use it as the name - if instanceState.Spec.DisplayName != "" { - instanceState.ObjectMeta.Name = instanceState.Spec.DisplayName + + if instanceState.ObjectMeta.Name == "" { + mLog.Errorf("Instance name is empty: catalog - %s", name) + return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Empty instance name: catalog - %s", name), v1alpha2.BadRequest) + } + + instanceName := instanceState.ObjectMeta.Name + parts := strings.Split(instanceName, ":") + if len(parts) == 2 { + instanceState.Spec.RootResource = parts[0] + instanceState.Spec.Version = parts[1] + } else { + mLog.Errorf("Instance name is invalid: instance - %s, catalog - %s", instanceName, name) + return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Instance name is invalid: catalog - %s", name), v1alpha2.BadRequest) } - instanceState.ObjectMeta = updateObjectMeta(instanceState.ObjectMeta, inputs, name) + + instanceState.ObjectMeta = updateObjectMeta(instanceState.ObjectMeta, inputs) objectData, _ := json.Marshal(instanceState) mLog.Debugf(" P (Materialize Processor): materialize instance %v to namespace %s", instanceState.ObjectMeta.Name, instanceState.ObjectMeta.Namespace) err = i.ApiClient.CreateInstance(ctx, instanceState.ObjectMeta.Name, objectData, instanceState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) @@ -179,11 +191,23 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to unmarshal solution state for catalog %s: %s: %s", name, err.Error()) return outputs, false, err } - // If inner solution defines a display name, use it as the name - if solutionState.Spec.DisplayName != "" { - solutionState.ObjectMeta.Name = solutionState.Spec.DisplayName + + if solutionState.ObjectMeta.Name == "" { + mLog.Errorf("Solution name is empty: catalog - %s", name) + return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Empty solution name: catalog - %s", name), v1alpha2.BadRequest) } - solutionState.ObjectMeta = updateObjectMeta(solutionState.ObjectMeta, inputs, name) + + solutionName := solutionState.ObjectMeta.Name + parts := strings.Split(solutionName, ":") + if len(parts) == 2 { + solutionState.Spec.RootResource = parts[0] + solutionState.Spec.Version = parts[1] + } else { + mLog.Errorf("Solution name is invalid: solution - %s, catalog - %s", solutionName, name) + return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid solution name: catalog - %s", name), v1alpha2.BadRequest) + } + + solutionState.ObjectMeta = updateObjectMeta(solutionState.ObjectMeta, inputs) objectData, _ := json.Marshal(solutionState) mLog.Debugf(" P (Materialize Processor): materialize solution %v to namespace %s", solutionState.ObjectMeta.Name, solutionState.ObjectMeta.Namespace) err = i.ApiClient.UpsertSolution(ctx, solutionState.ObjectMeta.Name, objectData, solutionState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) @@ -199,11 +223,23 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to unmarshal target state for catalog %s: %s", name, err.Error()) return outputs, false, err } - // If inner target defines a display name, use it as the name - if targetState.Spec.DisplayName != "" { - targetState.ObjectMeta.Name = targetState.Spec.DisplayName + + if targetState.ObjectMeta.Name == "" { + mLog.Errorf("Target name is empty: catalog - %s", name) + return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Empty target name: catalog - %s", name), v1alpha2.BadRequest) } - targetState.ObjectMeta = updateObjectMeta(targetState.ObjectMeta, inputs, name) + + targetName := targetState.ObjectMeta.Name + parts := strings.Split(targetName, ":") + if len(parts) == 2 { + targetState.Spec.RootResource = parts[0] + targetState.Spec.Version = parts[1] + } else { + mLog.Errorf("Target name is invalid: target - %s, catalog - %s", targetName, name) + return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid target name: %s", name), v1alpha2.BadRequest) + } + + targetState.ObjectMeta = updateObjectMeta(targetState.ObjectMeta, inputs) objectData, _ := json.Marshal(targetState) mLog.Debugf(" P (Materialize Processor): materialize target %v to namespace %s", targetState.ObjectMeta.Name, targetState.ObjectMeta.Namespace) err = i.ApiClient.CreateTarget(ctx, targetState.ObjectMeta.Name, objectData, targetState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) @@ -220,7 +256,23 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to unmarshal catalog state for catalog %s: %s", name, err.Error()) return outputs, false, err } - catalogState.ObjectMeta = updateObjectMeta(catalogState.ObjectMeta, inputs, name) + + if catalogState.ObjectMeta.Name == "" { + mLog.Errorf("Catalog name is empty %s", name) + return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Empty catalog name: %s", name), v1alpha2.BadRequest) + } + + catalogName := catalogState.ObjectMeta.Name + parts := strings.Split(catalogName, ":") + if len(parts) == 2 { + catalogState.Spec.RootResource = parts[0] + catalogState.Spec.Version = parts[1] + } else { + mLog.Errorf("Catalog name is invalid: catalog - %s, parent catalog - %s", catalogName, name) + return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid catalog name: catalog - %s", name), v1alpha2.BadRequest) + } + + catalogState.ObjectMeta = updateObjectMeta(catalogState.ObjectMeta, inputs) objectData, _ := json.Marshal(catalogState) mLog.Debugf(" P (Materialize Processor): materialize catalog %v to namespace %s", catalogState.ObjectMeta.Name, catalogState.ObjectMeta.Namespace) err = i.ApiClient.UpsertCatalog(ctx, catalogState.ObjectMeta.Name, objectData, i.Config.User, i.Config.Password) @@ -240,10 +292,9 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, nil } -func updateObjectMeta(objectMeta model.ObjectMeta, inputs map[string]interface{}, catalogName string) model.ObjectMeta { - if objectMeta.Name == "" { - // use the same name as catalog wrapping it if not provided - objectMeta.Name = catalogName +func updateObjectMeta(objectMeta model.ObjectMeta, inputs map[string]interface{}) model.ObjectMeta { + if strings.Contains(objectMeta.Name, ":") { + objectMeta.Name = strings.ReplaceAll(objectMeta.Name, ":", "-") } // stage inputs override objectMeta namespace if s := stage.GetNamespace(inputs); s != "" { diff --git a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize_test.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize_test.go index 39c596f1d..31b7ecaaa 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize_test.go +++ b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize_test.go @@ -146,13 +146,21 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { body, _ := io.ReadAll(r.Body) switch r.URL.Path { case "/instances/instance1-v1": - var instance model.InstanceState + instance := model.InstanceState{ + ObjectMeta: model.ObjectMeta{ + Name: "instance1-v1", + }, + } err := json.Unmarshal(body, &instance) assert.Nil(t, err) assert.Equal(t, expectNs, instance.ObjectMeta.Namespace) response = instance case "/targets/registry/target1-v1": - var target model.TargetState + target := model.TargetState{ + ObjectMeta: model.ObjectMeta{ + Name: "target1-v1", + }, + } err := json.Unmarshal(body, &target) assert.Nil(t, err) assert.Equal(t, expectNs, target.ObjectMeta.Namespace) @@ -176,6 +184,7 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { DisplayName: "target1", }, "metadata": &model.ObjectMeta{ + Name: "target1:v1", Namespace: "objNS", }, }, @@ -191,7 +200,7 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { "spec": model.InstanceSpec{}, "metadata": &model.ObjectMeta{ Namespace: "objNS", - Name: "instance1", + Name: "instance1:v1", }, }, }, @@ -208,6 +217,7 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { }, "metadata": &model.ObjectMeta{ Namespace: "objNS", + Name: "instance1:v1", }, }, }, @@ -225,7 +235,7 @@ func InitializeMockSymphonyAPI(t *testing.T, expectNs string) *httptest.Server { }, "metadata": &model.ObjectMeta{ Namespace: "objNS", - Name: "catalog1", + Name: "catalog1:v1", }, }, }, diff --git a/docs/samples/k8s/hello-world/instance-container.yaml b/docs/samples/k8s/hello-world/instance-container.yaml new file mode 100644 index 000000000..750a922ed --- /dev/null +++ b/docs/samples/k8s/hello-world/instance-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: InstanceContainer +metadata: + name: sampleprometheusinstance +spec: diff --git a/docs/samples/k8s/hello-world/instance.yaml b/docs/samples/k8s/hello-world/instance.yaml index b4883c06c..e6bb6f09f 100644 --- a/docs/samples/k8s/hello-world/instance.yaml +++ b/docs/samples/k8s/hello-world/instance.yaml @@ -1,9 +1,9 @@ apiVersion: solution.symphony/v1 kind: Instance metadata: - name: sample-prometheus-instance-v1 + name: sampleprometheusinstance-v1 spec: scope: sample-k8s-scope - solution: sample-prometheus-server:v1 + solution: sampleprometheusserver:v1 target: - name: sample-k8s-target:v1 \ No newline at end of file + name: samplek8starget:v1 \ No newline at end of file diff --git a/docs/samples/k8s/hello-world/solution-container.yaml b/docs/samples/k8s/hello-world/solution-container.yaml new file mode 100644 index 000000000..3fa8fb612 --- /dev/null +++ b/docs/samples/k8s/hello-world/solution-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: SolutionContainer +metadata: + name: sampleprometheusserver +spec: diff --git a/docs/samples/k8s/hello-world/solution.yaml b/docs/samples/k8s/hello-world/solution.yaml index 047059954..0afa6aaee 100644 --- a/docs/samples/k8s/hello-world/solution.yaml +++ b/docs/samples/k8s/hello-world/solution.yaml @@ -1,8 +1,9 @@ apiVersion: solution.symphony/v1 kind: Solution metadata: - name: sample-prometheus-server-v1 -spec: + name: sampleprometheusserver-v1 +spec: + rootResource: sampleprometheusserver metadata: deployment.replicas: "#1" service.ports: "[{\"name\":\"port9090\",\"port\": 9090}]" diff --git a/docs/samples/k8s/hello-world/target-container.yaml b/docs/samples/k8s/hello-world/target-container.yaml new file mode 100644 index 000000000..7f6b30402 --- /dev/null +++ b/docs/samples/k8s/hello-world/target-container.yaml @@ -0,0 +1,5 @@ +apiVersion: fabric.symphony/v1 +kind: TargetContainer +metadata: + name: samplek8starget +spec: diff --git a/docs/samples/k8s/hello-world/target.yaml b/docs/samples/k8s/hello-world/target.yaml index e9422dc11..365ced286 100644 --- a/docs/samples/k8s/hello-world/target.yaml +++ b/docs/samples/k8s/hello-world/target.yaml @@ -1,8 +1,9 @@ apiVersion: fabric.symphony/v1 kind: Target metadata: - name: sample-k8s-target-v1 -spec: + name: samplek8starget-v1 +spec: + rootResource: samplek8starget forceRedeploy: true topologies: - bindings: diff --git a/docs/samples/multisite/activation.yaml b/docs/samples/multisite/activation.yaml index bc4e5ad2d..1d895761a 100644 --- a/docs/samples/multisite/activation.yaml +++ b/docs/samples/multisite/activation.yaml @@ -3,5 +3,5 @@ kind: Activation metadata: name: multisite-deploy spec: - campaign: "site-apps:v1" + campaign: "siteapps:v1" \ No newline at end of file diff --git a/docs/samples/multisite/campaign.yaml b/docs/samples/multisite/campaign.yaml index 9993174e3..7673afbc9 100644 --- a/docs/samples/multisite/campaign.yaml +++ b/docs/samples/multisite/campaign.yaml @@ -1,8 +1,9 @@ apiVersion: workflow.symphony/v1 kind: Campaign metadata: - name: site-apps-v1 + name: siteapps-v1 spec: + rootResource: siteapps firstStage: list stages: list: diff --git a/docs/samples/multisite/catalog-catalog.yaml b/docs/samples/multisite/catalog-catalog.yaml index fd9d192c2..ba70f7a34 100644 --- a/docs/samples/multisite/catalog-catalog.yaml +++ b/docs/samples/multisite/catalog-catalog.yaml @@ -1,12 +1,13 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: - name: site-catalog-v1 + name: sitecatalog-v1 spec: + rootResource: sitecatalog catalogType: catalog properties: metadata: - name: web-app-config-v1 + name: webappconfig:v1 spec: type: config properties: diff --git a/docs/samples/multisite/instance-catalog.yaml b/docs/samples/multisite/instance-catalog.yaml index e7ee37b03..8a657d828 100644 --- a/docs/samples/multisite/instance-catalog.yaml +++ b/docs/samples/multisite/instance-catalog.yaml @@ -1,12 +1,15 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: - name: site-instance-v1 + name: siteinstance-v1 spec: + rootResource: siteinstance catalogType: instance properties: + metadata: + name: siteinstance:v1 spec: - solution: site-app:v1 + solution: siteapp:v1 target: selector: group: site \ No newline at end of file diff --git a/docs/samples/multisite/solution-catalog.yaml b/docs/samples/multisite/solution-catalog.yaml index bb683683d..3a7d26da8 100644 --- a/docs/samples/multisite/solution-catalog.yaml +++ b/docs/samples/multisite/solution-catalog.yaml @@ -1,18 +1,21 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: - name: site-app-v1 + name: siteapp-v1 spec: + rootResource: siteapp catalogType: solution properties: - spec: + metadata: + name: siteapp:v1 + spec: components: - name: web-app type: container metadata: service.ports: "[{\"name\":\"port3011\",\"port\": 3011,\"targetPort\":5000}]" - service.type: "${{$config('web-app-config:v1','serviceType')}}" + service.type: "${{$config('webappconfig:v1','serviceType')}}" properties: deployment.replicas: "#1" container.ports: "[{\"containerPort\":5000,\"protocol\":\"TCP\"}]" - container.image: "${{$config('web-app-config:v1','image')}}" \ No newline at end of file + container.image: "${{$config('webappconfig:v1','image')}}" diff --git a/docs/samples/multisite/target-catalog.yaml b/docs/samples/multisite/target-catalog.yaml index 419cb1902..7fb0404d0 100644 --- a/docs/samples/multisite/target-catalog.yaml +++ b/docs/samples/multisite/target-catalog.yaml @@ -1,10 +1,13 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: - name: site-k8s-target-v1 -spec: + name: sitek8starget-v1 +spec: + rootResource: sitek8starget catalogType: target properties: + metadata: + name: sitek8starget:v1 spec: properties: group: site diff --git a/k8s/apis/fabric/v1/target_webhook.go b/k8s/apis/fabric/v1/target_webhook.go index 9f03d5206..ca97f3be4 100644 --- a/k8s/apis/fabric/v1/target_webhook.go +++ b/k8s/apis/fabric/v1/target_webhook.go @@ -17,6 +17,7 @@ import ( configutils "gopls-workspace/configutils" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" @@ -81,6 +82,25 @@ func (r *Target) Default() { if r.Spec.ReconciliationPolicy != nil && r.Spec.ReconciliationPolicy.State == "" { r.Spec.ReconciliationPolicy.State = v1.ReconciliationPolicy_Active } + + if r.Spec.RootResource != "" { + var targetContainer TargetContainer + err := myTargetClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &targetContainer) + if err != nil { + targetlog.Error(err, "failed to get target container", "name", r.Spec.RootResource) + } else { + ownerReference := metav1.OwnerReference{ + APIVersion: targetContainer.APIVersion, + Kind: targetContainer.Kind, + Name: targetContainer.Name, + UID: targetContainer.UID, + } + + if !configutils.CheckOwnerReferenceAlreadySet(r.OwnerReferences, ownerReference) { + r.OwnerReferences = append(r.OwnerReferences, ownerReference) + } + } + } } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. @@ -159,6 +179,12 @@ func (r *Target) validateCreateTarget() error { if err := r.validateReconciliationPolicy(); err != nil { allErrs = append(allErrs, err) } + if err := r.validateNameOnCreate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateRootResource(); err != nil { + allErrs = append(allErrs, err) + } if len(allErrs) == 0 { return nil @@ -236,6 +262,24 @@ func (r *Target) validateReconciliationPolicy() *field.Error { return nil } +func (r *Target) validateNameOnCreate() *field.Error { + return configutils.ValidateObjectName(r.ObjectMeta.Name, r.Spec.RootResource) +} + +func (r *Target) validateRootResource() *field.Error { + var targetContainer TargetContainer + err := myTargetClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &targetContainer) + if err != nil { + return field.Invalid(field.NewPath("spec").Child("rootResource"), r.Spec.RootResource, "rootResource must be a valid target container") + } + + if len(r.ObjectMeta.OwnerReferences) == 0 { + return field.Invalid(field.NewPath("metadata").Child("ownerReference"), len(r.ObjectMeta.OwnerReferences), "ownerReference must be set") + } + + return nil +} + func (r *Target) validateUpdateTarget() error { var allErrs field.ErrorList if err := r.validateUniqueNameOnUpdate(); err != nil { diff --git a/k8s/apis/federation/v1/catalog_webhook.go b/k8s/apis/federation/v1/catalog_webhook.go index 8db02b804..c82724527 100644 --- a/k8s/apis/federation/v1/catalog_webhook.go +++ b/k8s/apis/federation/v1/catalog_webhook.go @@ -9,13 +9,16 @@ package v1 import ( "context" "encoding/json" - "fmt" "gopls-workspace/apis/metrics/v1" + "gopls-workspace/configutils" "time" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -58,6 +61,25 @@ var _ webhook.Defaulter = &Catalog{} // Default implements webhook.Defaulter so a webhook will be registered for the type func (r *Catalog) Default() { cataloglog.Info("default", "name", r.Name) + + if r.Spec.RootResource != "" { + var catalogContainer CatalogContainer + err := myCatalogClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &catalogContainer) + if err != nil { + cataloglog.Error(err, "failed to get catalog container", "name", r.Spec.RootResource) + } else { + ownerReference := metav1.OwnerReference{ + APIVersion: catalogContainer.APIVersion, + Kind: catalogContainer.Kind, + Name: catalogContainer.Name, + UID: catalogContainer.UID, + } + + if !configutils.CheckOwnerReferenceAlreadySet(r.OwnerReferences, ownerReference) { + r.OwnerReferences = append(r.OwnerReferences, ownerReference) + } + } + } } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. @@ -120,11 +142,26 @@ func (r *Catalog) ValidateDelete() (admission.Warnings, error) { } func (r *Catalog) validateCreateCatalog() error { - return r.checkSchema() -} + var allErrs field.ErrorList -func (r *Catalog) checkSchema() error { + if err := r.checkSchema(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateNameOnCreate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateRootResource(); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(schema.GroupKind{Group: "federation.symphony", Kind: "Catalog"}, r.Name, allErrs) +} +func (r *Catalog) checkSchema() *field.Error { if r.Spec.Metadata != nil { if schemaName, ok := r.Spec.Metadata["schema"]; ok { cataloglog.Info("Find schema name", "name", schemaName) @@ -132,7 +169,7 @@ func (r *Catalog) checkSchema() error { err := myCatalogClient.List(context.Background(), &catalogs, client.InNamespace(r.ObjectMeta.Namespace), client.MatchingFields{".metadata.name": schemaName}) if err != nil || len(catalogs.Items) == 0 { cataloglog.Error(err, "Could not find the required schema.", "name", schemaName) - return apierrors.NewBadRequest(fmt.Sprintf("Could not find the required schema, %s.", schemaName)) + return field.Invalid(field.NewPath("spec").Child("Metadata"), schemaName, "could not find the required schema") } jData, _ := json.Marshal(catalogs.Items[0].Spec.Properties) @@ -140,7 +177,7 @@ func (r *Catalog) checkSchema() error { err = json.Unmarshal(jData, &properties) if err != nil { cataloglog.Error(err, "Invalid schema.", "name", schemaName) - return apierrors.NewBadRequest(fmt.Sprintf("Invalid schema, %s.", schemaName)) + return field.Invalid(field.NewPath("spec").Child("properties"), schemaName, "invalid catalog properties") } if spec, ok := properties["spec"]; ok { var schemaObj utils.Schema @@ -148,23 +185,23 @@ func (r *Catalog) checkSchema() error { err := json.Unmarshal(jData, &schemaObj) if err != nil { cataloglog.Error(err, "Invalid schema.", "name", schemaName) - return apierrors.NewBadRequest(fmt.Sprintf("Invalid schema, %s.", schemaName)) + return field.Invalid(field.NewPath("spec").Child("properties"), schemaName, "invalid schema") } jData, _ = json.Marshal(r.Spec.Properties) var properties map[string]interface{} err = json.Unmarshal(jData, &properties) if err != nil { cataloglog.Error(err, "Validating failed.") - return apierrors.NewBadRequest("Invalid properties of the catalog.") + return field.Invalid(field.NewPath("spec").Child("Properties"), schemaName, "unable to unmarshall properties of the catalog") } result, err := schemaObj.CheckProperties(properties, nil) if err != nil { cataloglog.Error(err, "Validating failed.") - return apierrors.NewBadRequest("Validate failed for the catalog.") + return field.Invalid(field.NewPath("spec").Child("Properties"), schemaName, "invalid properties of the catalog schema") } if !result.Valid { cataloglog.Error(err, "Validating failed.") - return apierrors.NewBadRequest("This is not a valid catalog according to the schema.") + return field.Invalid(field.NewPath("spec").Child("Properties"), schemaName, "invalid schema result") } } cataloglog.Info("Validation finished.", "name", r.Name) @@ -174,6 +211,35 @@ func (r *Catalog) checkSchema() error { } return nil } + func (r *Catalog) validateUpdateCatalog() error { - return r.checkSchema() + var allErrs field.ErrorList + + if err := r.checkSchema(); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(schema.GroupKind{Group: "solution.symphony", Kind: "Solution"}, r.Name, allErrs) +} + +func (r *Catalog) validateNameOnCreate() *field.Error { + return configutils.ValidateObjectName(r.ObjectMeta.Name, r.Spec.RootResource) +} + +func (r *Catalog) validateRootResource() *field.Error { + var catalogContainer CatalogContainer + err := myCatalogClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &catalogContainer) + if err != nil { + return field.Invalid(field.NewPath("spec").Child("rootResource"), r.Spec.RootResource, "rootResource must be a valid catalog container") + } + + if len(r.ObjectMeta.OwnerReferences) == 0 { + return field.Invalid(field.NewPath("metadata").Child("ownerReference"), len(r.ObjectMeta.OwnerReferences), "ownerReference must be set") + } + + return nil } diff --git a/k8s/apis/solution/v1/instance_webhook.go b/k8s/apis/solution/v1/instance_webhook.go index 3de369ccf..c6a8eea77 100644 --- a/k8s/apis/solution/v1/instance_webhook.go +++ b/k8s/apis/solution/v1/instance_webhook.go @@ -11,8 +11,11 @@ import ( "fmt" "gopls-workspace/apis/metrics/v1" v1 "gopls-workspace/apis/model/v1" + "gopls-workspace/configutils" "time" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" @@ -21,8 +24,6 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - apierrors "k8s.io/apimachinery/pkg/api/errors" ) // log is for logging in this package. @@ -72,6 +73,25 @@ func (r *Instance) Default() { if r.Spec.ReconciliationPolicy != nil && r.Spec.ReconciliationPolicy.State == "" { r.Spec.ReconciliationPolicy.State = v1.ReconciliationPolicy_Active } + + if r.Spec.RootResource != "" { + var instanceContainer InstanceContainer + err := myInstanceClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &instanceContainer) + if err != nil { + instancelog.Error(err, "failed to get instance container", "name", r.Spec.RootResource) + } else { + ownerReference := metav1.OwnerReference{ + APIVersion: instanceContainer.APIVersion, + Kind: instanceContainer.Kind, + Name: instanceContainer.Name, + UID: instanceContainer.UID, + } + + if !configutils.CheckOwnerReferenceAlreadySet(r.OwnerReferences, ownerReference) { + r.OwnerReferences = append(r.OwnerReferences, ownerReference) + } + } + } } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. @@ -147,6 +167,12 @@ func (r *Instance) validateCreateInstance() error { if err := r.validateReconciliationPolicy(); err != nil { allErrs = append(allErrs, err) } + if err := r.validateNameOnCreate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateRootResource(); err != nil { + allErrs = append(allErrs, err) + } if len(allErrs) == 0 { return nil @@ -216,3 +242,21 @@ func (r *Instance) validateReconciliationPolicy() *field.Error { return nil } + +func (r *Instance) validateNameOnCreate() *field.Error { + return configutils.ValidateObjectName(r.ObjectMeta.Name, r.Spec.RootResource) +} + +func (r *Instance) validateRootResource() *field.Error { + var instanceContainer InstanceContainer + err := myInstanceClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &instanceContainer) + if err != nil { + return field.Invalid(field.NewPath("spec").Child("rootResource"), r.Spec.RootResource, "rootResource must be a valid instance container") + } + + if len(r.ObjectMeta.OwnerReferences) == 0 { + return field.Invalid(field.NewPath("metadata").Child("ownerReference"), len(r.ObjectMeta.OwnerReferences), "ownerReference must be set") + } + + return nil +} diff --git a/k8s/apis/solution/v1/solution_webhook.go b/k8s/apis/solution/v1/solution_webhook.go index ec32fa594..06867445f 100644 --- a/k8s/apis/solution/v1/solution_webhook.go +++ b/k8s/apis/solution/v1/solution_webhook.go @@ -9,9 +9,13 @@ package v1 import ( "context" "fmt" + "gopls-workspace/configutils" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" @@ -26,8 +30,8 @@ var mySolutionClient client.Client func (r *Solution) SetupWebhookWithManager(mgr ctrl.Manager) error { mySolutionClient = mgr.GetClient() mgr.GetFieldIndexer().IndexField(context.Background(), &Solution{}, ".spec.displayName", func(rawObj client.Object) []string { - target := rawObj.(*Solution) - return []string{target.Spec.DisplayName} + solution := rawObj.(*Solution) + return []string{solution.Spec.DisplayName} }) return ctrl.NewWebhookManagedBy(mgr). For(r). @@ -47,6 +51,25 @@ func (r *Solution) Default() { if r.Spec.DisplayName == "" { r.Spec.DisplayName = r.ObjectMeta.Name } + + if r.Spec.RootResource != "" { + var solutionContainer SolutionContainer + err := mySolutionClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &solutionContainer) + if err != nil { + solutionlog.Error(err, "failed to get solution container", "name", r.Spec.RootResource) + } else { + ownerReference := metav1.OwnerReference{ + APIVersion: solutionContainer.APIVersion, + Kind: solutionContainer.Kind, + Name: solutionContainer.Name, + UID: solutionContainer.UID, + } + + if !configutils.CheckOwnerReferenceAlreadySet(r.OwnerReferences, ownerReference) { + r.OwnerReferences = append(r.OwnerReferences, ownerReference) + } + } + } } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. @@ -77,12 +100,23 @@ func (r *Solution) ValidateDelete() (admission.Warnings, error) { } func (r *Solution) validateCreateSolution() error { - var solutions SolutionList - mySolutionClient.List(context.Background(), &solutions, client.InNamespace(r.Namespace), client.MatchingFields{".spec.displayName": r.Spec.DisplayName}) - if len(solutions.Items) != 0 { - return apierrors.NewBadRequest(fmt.Sprintf("solution display name '%s' is already taken", r.Spec.DisplayName)) + var allErrs field.ErrorList + + if err := r.validateUniqueNameOnCreate(); err != nil { + allErrs = append(allErrs, err) } - return nil + if err := r.validateNameOnCreate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateRootResource(); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(schema.GroupKind{Group: "solution.symphony", Kind: "Solution"}, r.Name, allErrs) } func (r *Solution) validateUpdateSolution() error { @@ -96,3 +130,35 @@ func (r *Solution) validateUpdateSolution() error { } return nil } + +func (r *Solution) validateUniqueNameOnCreate() *field.Error { + var solutions SolutionList + err := mySolutionClient.List(context.Background(), &solutions, client.InNamespace(r.Namespace), client.MatchingFields{".spec.displayName": r.Spec.DisplayName}) + if err != nil { + return field.InternalError(&field.Path{}, err) + } + + if len(solutions.Items) != 0 { + return field.Invalid(field.NewPath("spec").Child("displayName"), r.Spec.DisplayName, "solution display name is already taken") + } + + return nil +} + +func (r *Solution) validateNameOnCreate() *field.Error { + return configutils.ValidateObjectName(r.ObjectMeta.Name, r.Spec.RootResource) +} + +func (r *Solution) validateRootResource() *field.Error { + var solutionContainer SolutionContainer + err := mySolutionClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &solutionContainer) + if err != nil { + return field.Invalid(field.NewPath("spec").Child("rootResource"), r.Spec.RootResource, "rootResource must be a valid solution container") + } + + if len(r.ObjectMeta.OwnerReferences) == 0 { + return field.Invalid(field.NewPath("metadata").Child("ownerReference"), len(r.ObjectMeta.OwnerReferences), "ownerReference must be set") + } + + return nil +} diff --git a/k8s/apis/workflow/v1/campaign_webhook.go b/k8s/apis/workflow/v1/campaign_webhook.go new file mode 100644 index 000000000..16047f51d --- /dev/null +++ b/k8s/apis/workflow/v1/campaign_webhook.go @@ -0,0 +1,160 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package v1 + +import ( + "context" + "gopls-workspace/apis/metrics/v1" + "gopls-workspace/configutils" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var campaignlog = logf.Log.WithName("campaign-resource") +var myCampaignClient client.Client +var catalogWebhookValidationMetrics *metrics.Metrics + +func (r *Campaign) SetupWebhookWithManager(mgr ctrl.Manager) error { + myCampaignClient = mgr.GetClient() + mgr.GetFieldIndexer().IndexField(context.Background(), &Campaign{}, ".metadata.name", func(rawObj client.Object) []string { + campaign := rawObj.(*Campaign) + return []string{campaign.Name} + }) + + // initialize the controller operation metrics + if catalogWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + catalogWebhookValidationMetrics = metrics + } + + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-workflow-symphony-v1-campaign,mutating=true,failurePolicy=fail,sideEffects=None,groups=workflow.symphony,resources=campaigns,verbs=create;update,versions=v1,name=mcampaign.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &Campaign{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *Campaign) Default() { + campaignlog.Info("default", "name", r.Name) + + if r.Spec.RootResource != "" { + var campaignContainer CampaignContainer + err := myCampaignClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &campaignContainer) + if err != nil { + campaignlog.Error(err, "failed to get campaign container", "name", r.Spec.RootResource) + } else { + ownerReference := metav1.OwnerReference{ + APIVersion: campaignContainer.APIVersion, + Kind: campaignContainer.Kind, + Name: campaignContainer.Name, + UID: campaignContainer.UID, + } + + if !configutils.CheckOwnerReferenceAlreadySet(r.OwnerReferences, ownerReference) { + r.OwnerReferences = append(r.OwnerReferences, ownerReference) + } + } + } +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. + +//+kubebuilder:webhook:path=/validate-workflow-symphony-v1-campaign,mutating=false,failurePolicy=fail,sideEffects=None,groups=workflow.symphony,resources=campaigns,verbs=create;update,versions=v1,name=vcampaign.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &Campaign{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Campaign) ValidateCreate() (admission.Warnings, error) { + campaignlog.Info("validate create", "name", r.Name) + + validateCreateTime := time.Now() + validationError := r.validateCreateCampaign() + if validationError != nil { + catalogWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.CatalogResourceType) + } else { + catalogWebhookValidationMetrics.ControllerValidationLatency( + validateCreateTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.CatalogResourceType) + } + + return nil, validationError +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Campaign) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + campaignlog.Info("validate update", "name", r.Name) + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Campaign) ValidateDelete() (admission.Warnings, error) { + campaignlog.Info("validate delete", "name", r.Name) + + return nil, nil +} + +func (r *Campaign) validateCreateCampaign() error { + var allErrs field.ErrorList + + if err := r.validateNameOnCreate(); err != nil { + allErrs = append(allErrs, err) + } + if err := r.validateRootResource(); err != nil { + allErrs = append(allErrs, err) + } + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid(schema.GroupKind{Group: "workflow.symphony", Kind: "Campaign"}, r.Name, allErrs) +} + +func (r *Campaign) validateNameOnCreate() *field.Error { + return configutils.ValidateObjectName(r.ObjectMeta.Name, r.Spec.RootResource) +} + +func (r *Campaign) validateRootResource() *field.Error { + var campaignContainer CampaignContainer + err := myCampaignClient.Get(context.Background(), client.ObjectKey{Name: r.Spec.RootResource, Namespace: r.Namespace}, &campaignContainer) + if err != nil { + return field.Invalid(field.NewPath("spec").Child("rootResource"), r.Spec.RootResource, "rootResource must be a valid campaign container") + } + + campaignlog.Info("validate OwnerReferences", "name", r.Name) + if len(r.ObjectMeta.OwnerReferences) == 0 { + return field.Invalid(field.NewPath("metadata").Child("ownerReference"), len(r.ObjectMeta.OwnerReferences), "ownerReference must be set") + } + + return nil +} diff --git a/k8s/config/oss/webhook/manifests.yaml b/k8s/config/oss/webhook/manifests.yaml index ffd43d4d3..0f97a9cc7 100644 --- a/k8s/config/oss/webhook/manifests.yaml +++ b/k8s/config/oss/webhook/manifests.yaml @@ -64,6 +64,26 @@ webhooks: resources: - solutions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-workflow-symphony-v1-campaign + failurePolicy: Fail + name: mcampaign.kb.io + rules: + - apiGroups: + - workflow.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - campaigns + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -150,6 +170,26 @@ webhooks: resources: - solutions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-workflow-symphony-v1-campaign + failurePolicy: Fail + name: vcampaign.kb.io + rules: + - apiGroups: + - workflow.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - campaigns + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/k8s/configutils/configutil.go b/k8s/configutils/configutil.go index a050f58fa..28d078da0 100644 --- a/k8s/configutils/configutil.go +++ b/k8s/configutils/configutil.go @@ -10,11 +10,14 @@ import ( "context" "io/ioutil" "os" + "strings" configv1 "gopls-workspace/apis/config/v1" "gopls-workspace/constants" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "sigs.k8s.io/yaml" @@ -86,3 +89,44 @@ func CheckValidationPack(myName string, myValue, validationType string, pack []c } return "", nil } + +func CheckOwnerReferenceAlreadySet(existingRefs []metav1.OwnerReference, ownerRefToCheck metav1.OwnerReference) bool { + for _, r := range existingRefs { + if areSameOwnerReferences(ownerRefToCheck, r) { + return true + } + } + return false +} + +// Returns true if a and b point to the same object +func areSameOwnerReferences(a, b metav1.OwnerReference) bool { + aGV, err := schema.ParseGroupVersion(a.APIVersion) + if err != nil { + return false + } + + bGV, err := schema.ParseGroupVersion(b.APIVersion) + if err != nil { + return false + } + + return aGV == bGV && a.Kind == b.Kind && a.Name == b.Name +} + +func ValidateObjectName(name string, rootResource string) *field.Error { + if rootResource == "" { + return field.Invalid(field.NewPath("spec").Child("rootResource"), rootResource, "rootResource must be a non-empty string") + } + + parts := strings.Split(name, "-") + if len(parts) != 2 { + return field.Invalid(field.NewPath("name"), name, "name must be in the format of - and only one hyphen is allowed") + } + + if parts[0] != rootResource { + return field.Invalid(field.NewPath("name"), name, "name must start with spec.rootResource") + } + + return nil +} diff --git a/k8s/main.go b/k8s/main.go index 1002b9c11..aac9b1c8f 100644 --- a/k8s/main.go +++ b/k8s/main.go @@ -298,6 +298,10 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "Catalog") os.Exit(1) } + if err = (&workflowv1.Campaign{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Campaign") + os.Exit(1) + } } if err = (&solutioncontrollers.SolutionContainerReconciler{ Client: mgr.GetClient(), diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index ee128f5b4..fcdb48a51 100644 --- a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -2705,6 +2705,26 @@ webhooks: resources: - solutions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-workflow-symphony-v1-campaign + failurePolicy: Fail + name: mcampaign.kb.io + rules: + - apiGroups: + - workflow.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - campaigns + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2794,6 +2814,26 @@ webhooks: resources: - solutions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-workflow-symphony-v1-campaign + failurePolicy: Fail + name: vcampaign.kb.io + rules: + - apiGroups: + - workflow.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - campaigns + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/test/integration/lib/testhelpers/manifestbuilder.go b/test/integration/lib/testhelpers/manifestbuilder.go index 25b6053d5..a9ecbe829 100644 --- a/test/integration/lib/testhelpers/manifestbuilder.go +++ b/test/integration/lib/testhelpers/manifestbuilder.go @@ -129,6 +129,10 @@ type ( Properties map[string]string PostProcess func(*Target) } + + ContainerOptions = struct { + Namespace string + } ) const ( @@ -258,3 +262,57 @@ func PatchInstance(data []byte, opts InstanceOptions) ([]byte, error) { } return yaml.Marshal(instance) } + +func PatchSolutionContainer(data []byte, opts InstanceOptions) ([]byte, error) { + var instanceContainer InstanceContainer + err := yaml.Unmarshal(data, &instanceContainer) + if err != nil { + return nil, err + } + + if opts.Namespace != "" { + instanceContainer.Metadata.Namespace = opts.Namespace + } + + if instanceContainer.Metadata.Annotations == nil { + instanceContainer.Metadata.Annotations = make(map[string]string) + } + + return yaml.Marshal(instanceContainer) +} + +func PatchTargetContainer(data []byte, opts InstanceOptions) ([]byte, error) { + var targetContainer TargetContainer + err := yaml.Unmarshal(data, &targetContainer) + if err != nil { + return nil, err + } + + if opts.Namespace != "" { + targetContainer.Metadata.Namespace = opts.Namespace + } + + if targetContainer.Metadata.Annotations == nil { + targetContainer.Metadata.Annotations = make(map[string]string) + } + + return yaml.Marshal(targetContainer) +} + +func PatchInstanceContainer(data []byte, opts InstanceOptions) ([]byte, error) { + var instanceContainer InstanceContainer + err := yaml.Unmarshal(data, &instanceContainer) + if err != nil { + return nil, err + } + + if opts.Namespace != "" { + instanceContainer.Metadata.Namespace = opts.Namespace + } + + if instanceContainer.Metadata.Annotations == nil { + instanceContainer.Metadata.Annotations = make(map[string]string) + } + + return yaml.Marshal(instanceContainer) +} diff --git a/test/integration/lib/testhelpers/types.go b/test/integration/lib/testhelpers/types.go index adfa0bbb2..734c5a9e9 100644 --- a/test/integration/lib/testhelpers/types.go +++ b/test/integration/lib/testhelpers/types.go @@ -17,10 +17,11 @@ type ( } SolutionSpec struct { - DisplayName string `yaml:"displayName,omitempty"` - Scope string `yaml:"scope,omitempty"` - Metadata map[string]string `yaml:"metadata,omitempty"` - Components []ComponentSpec `yaml:"components,omitempty"` + DisplayName string `yaml:"displayName,omitempty"` + Scope string `yaml:"scope,omitempty"` + Metadata map[string]string `yaml:"metadata,omitempty"` + Components []ComponentSpec `yaml:"components,omitempty"` + RootResource string `yaml:"rootResource"` } // Target describes the structure of symphony target yaml file @@ -32,11 +33,12 @@ type ( } TargetSpec struct { - DisplayName string `yaml:"displayName"` - Scope string `yaml:"scope"` - Components []ComponentSpec `yaml:"components,omitempty"` - Topologies []Topology `yaml:"topologies"` - Properties map[string]string `yaml:"properties,omitempty"` + DisplayName string `yaml:"displayName"` + Scope string `yaml:"scope"` + Components []ComponentSpec `yaml:"components,omitempty"` + Topologies []Topology `yaml:"topologies"` + Properties map[string]string `yaml:"properties,omitempty"` + RootResource string `yaml:"rootResource"` } Topology struct { @@ -70,11 +72,12 @@ type ( } InstanceSpec struct { - DisplayName string `yaml:"displayName"` - Target TargetSelector `yaml:"target"` - Solution string `yaml:"solution"` - Scope string `yaml:"scope"` - Parameters map[string]interface{} `yaml:"parameters,omitempty"` + DisplayName string `yaml:"displayName"` + Target TargetSelector `yaml:"target"` + Solution string `yaml:"solution"` + Scope string `yaml:"scope"` + Parameters map[string]interface{} `yaml:"parameters,omitempty"` + RootResource string `yaml:"rootResource"` } TargetSelector struct { @@ -86,4 +89,34 @@ type ( Type string `yaml:"type"` DefaultValue interface{} `yaml:"default"` } + + InstanceContainer struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata Metadata `yaml:"metadata"` + Spec InstanceContainerSpec `yaml:"spec"` + } + + InstanceContainerSpec struct { + } + + SolutionContainer struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata Metadata `yaml:"metadata"` + Spec SolutionContainerSpec `yaml:"spec"` + } + + SolutionContainerSpec struct { + } + + TargetContainer struct { + ApiVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata Metadata `yaml:"metadata"` + Spec TargetContainerSpec `yaml:"spec"` + } + + TargetContainerSpec struct { + } ) diff --git a/test/integration/scenarios/01.update/manifestTemplates/oss/instance-container.yaml b/test/integration/scenarios/01.update/manifestTemplates/oss/instance-container.yaml new file mode 100644 index 000000000..b39fde4e6 --- /dev/null +++ b/test/integration/scenarios/01.update/manifestTemplates/oss/instance-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: InstanceContainer +metadata: + name: instance +spec: diff --git a/test/integration/scenarios/01.update/manifestTemplates/oss/instance.yaml b/test/integration/scenarios/01.update/manifestTemplates/oss/instance.yaml index cd7d716d0..41e1cfcdc 100755 --- a/test/integration/scenarios/01.update/manifestTemplates/oss/instance.yaml +++ b/test/integration/scenarios/01.update/manifestTemplates/oss/instance.yaml @@ -9,8 +9,9 @@ metadata: annotations: {} name: instance-v1 spec: + rootResource: instance displayName: instance-v1 scope: alice-springs - solution: my-sol:v1 + solution: mysol:v1 target: name: self:v1 diff --git a/test/integration/scenarios/01.update/manifestTemplates/oss/solution-container.yaml b/test/integration/scenarios/01.update/manifestTemplates/oss/solution-container.yaml new file mode 100644 index 000000000..fb8b517dd --- /dev/null +++ b/test/integration/scenarios/01.update/manifestTemplates/oss/solution-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: SolutionContainer +metadata: + name: mysol +spec: diff --git a/test/integration/scenarios/01.update/manifestTemplates/oss/solution.yaml b/test/integration/scenarios/01.update/manifestTemplates/oss/solution.yaml index 9525ba151..3daba0ff4 100755 --- a/test/integration/scenarios/01.update/manifestTemplates/oss/solution.yaml +++ b/test/integration/scenarios/01.update/manifestTemplates/oss/solution.yaml @@ -7,6 +7,7 @@ apiVersion: solution.symphony/v1 kind: Solution metadata: annotations: {} - name: my-sol-v1 + name: mysol-v1 spec: + rootResource: mysol displayName: My solution diff --git a/test/integration/scenarios/01.update/manifestTemplates/oss/target-container.yaml b/test/integration/scenarios/01.update/manifestTemplates/oss/target-container.yaml new file mode 100644 index 000000000..8d545fa4d --- /dev/null +++ b/test/integration/scenarios/01.update/manifestTemplates/oss/target-container.yaml @@ -0,0 +1,5 @@ +apiVersion: fabric.symphony/v1 +kind: TargetContainer +metadata: + name: self +spec: diff --git a/test/integration/scenarios/01.update/manifestTemplates/oss/target.yaml b/test/integration/scenarios/01.update/manifestTemplates/oss/target.yaml index 0679581a7..d5105bfb3 100755 --- a/test/integration/scenarios/01.update/manifestTemplates/oss/target.yaml +++ b/test/integration/scenarios/01.update/manifestTemplates/oss/target.yaml @@ -9,6 +9,7 @@ metadata: name: self-v1 annotations: {} spec: + rootResource: self displayName: int-virtual-02-v1 scope: alice-springs topologies: diff --git a/test/integration/scenarios/01.update/verify/manifest_test.go b/test/integration/scenarios/01.update/verify/manifest_test.go index 4abe89a04..525b5972e 100644 --- a/test/integration/scenarios/01.update/verify/manifest_test.go +++ b/test/integration/scenarios/01.update/verify/manifest_test.go @@ -54,6 +54,12 @@ var ( var ( // Manifest templates + containerManifestTemplates = map[string]string{ + "target-container": fmt.Sprintf("%s/%s/target-container.yaml", manifestTemplateFolder, "oss"), + "instance-container": fmt.Sprintf("%s/%s/instance-container.yaml", manifestTemplateFolder, "oss"), + "solution-container": fmt.Sprintf("%s/%s/solution-container.yaml", manifestTemplateFolder, "oss"), + } + manifestTemplates = map[string]string{ "target": fmt.Sprintf("%s/%s/target.yaml", manifestTemplateFolder, "oss"), "instance": fmt.Sprintf("%s/%s/instance.yaml", manifestTemplateFolder, "oss"), @@ -122,6 +128,12 @@ func TestScenario_Update_AllNamespaces(t *testing.T) { func Scenario_Update(t *testing.T, namespace string) { // Deploy base manifests + for _, manifest := range containerManifestTemplates { + fullPath, err := filepath.Abs(manifest) + require.NoError(t, err) + err = shellcmd.Command(fmt.Sprintf("kubectl apply -f %s -n %s", fullPath, namespace)).Run() + require.NoError(t, err) + } for _, manifest := range manifestTemplates { fullPath, err := filepath.Abs(manifest) require.NoError(t, err) diff --git a/test/integration/scenarios/02.basic/magefile.go b/test/integration/scenarios/02.basic/magefile.go index 075fbe348..2f355be6b 100644 --- a/test/integration/scenarios/02.basic/magefile.go +++ b/test/integration/scenarios/02.basic/magefile.go @@ -35,6 +35,9 @@ var ( var ( // Manifests to deploy testManifests = []string{ + "manifest/%s/target-container.yaml", + "manifest/%s/instance-container.yaml", + "manifest/%s/solution-container.yaml", "manifest/%s/target.yaml", "manifest/%s/instance.yaml", "manifest/%s/solution.yaml", @@ -120,6 +123,9 @@ func DeployManifests(namespace string) error { return err } stringYaml := string(data) + stringYaml = strings.ReplaceAll(stringYaml, "INSTANCECONTAINERNAME", namespace+"instance") + stringYaml = strings.ReplaceAll(stringYaml, "TARGETCONTAINERNAME", namespace+"target") + stringYaml = strings.ReplaceAll(stringYaml, "SOLUTIONCONTAINERNAME", namespace+"solution") stringYaml = strings.ReplaceAll(stringYaml, "INSTANCENAME", namespace+"instance-v1") stringYaml = strings.ReplaceAll(stringYaml, "SCOPENAME", namespace+"scope") stringYaml = strings.ReplaceAll(stringYaml, "TARGETNAME", namespace+"target-v1") diff --git a/test/integration/scenarios/02.basic/manifest/oss/instance-container.yaml b/test/integration/scenarios/02.basic/manifest/oss/instance-container.yaml new file mode 100644 index 000000000..125593884 --- /dev/null +++ b/test/integration/scenarios/02.basic/manifest/oss/instance-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: InstanceContainer +metadata: + name: INSTANCECONTAINERNAME +spec: diff --git a/test/integration/scenarios/02.basic/manifest/oss/instance.yaml b/test/integration/scenarios/02.basic/manifest/oss/instance.yaml index f235f2776..bdfe5d449 100755 --- a/test/integration/scenarios/02.basic/manifest/oss/instance.yaml +++ b/test/integration/scenarios/02.basic/manifest/oss/instance.yaml @@ -9,6 +9,7 @@ metadata: annotations: {} name: INSTANCENAME spec: + rootResource: INSTANCECONTAINERNAME displayName: INSTANCENAME scope: SCOPENAME solution: SOLUTIONREFNAME diff --git a/test/integration/scenarios/02.basic/manifest/oss/solution-container.yaml b/test/integration/scenarios/02.basic/manifest/oss/solution-container.yaml new file mode 100644 index 000000000..6435f5240 --- /dev/null +++ b/test/integration/scenarios/02.basic/manifest/oss/solution-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: SolutionContainer +metadata: + name: SOLUTIONCONTAINERNAME +spec: diff --git a/test/integration/scenarios/02.basic/manifest/oss/solution.yaml b/test/integration/scenarios/02.basic/manifest/oss/solution.yaml index 5bbcd7ee6..ab4448d88 100755 --- a/test/integration/scenarios/02.basic/manifest/oss/solution.yaml +++ b/test/integration/scenarios/02.basic/manifest/oss/solution.yaml @@ -9,6 +9,7 @@ metadata: annotations: {} name: SOLUTIONNAME spec: + rootResource: SOLUTIONCONTAINERNAME components: - name: e4k-high-availability-broker properties: diff --git a/test/integration/scenarios/02.basic/manifest/oss/target-container.yaml b/test/integration/scenarios/02.basic/manifest/oss/target-container.yaml new file mode 100644 index 000000000..bd45b97fd --- /dev/null +++ b/test/integration/scenarios/02.basic/manifest/oss/target-container.yaml @@ -0,0 +1,5 @@ +apiVersion: fabric.symphony/v1 +kind: TargetContainer +metadata: + name: TARGETCONTAINERNAME +spec: diff --git a/test/integration/scenarios/02.basic/manifest/oss/target.yaml b/test/integration/scenarios/02.basic/manifest/oss/target.yaml index 9f2dd0317..0750b1826 100755 --- a/test/integration/scenarios/02.basic/manifest/oss/target.yaml +++ b/test/integration/scenarios/02.basic/manifest/oss/target.yaml @@ -8,6 +8,7 @@ kind: Target metadata: name: TARGETNAME spec: + rootResource: TARGETCONTAINERNAME components: - name: observability properties: diff --git a/test/integration/scenarios/03.basicWithNsDelete/magefile.go b/test/integration/scenarios/03.basicWithNsDelete/magefile.go index 94013c020..c4dcdba30 100644 --- a/test/integration/scenarios/03.basicWithNsDelete/magefile.go +++ b/test/integration/scenarios/03.basicWithNsDelete/magefile.go @@ -48,6 +48,9 @@ const ( var ( // Manifests to deploy testManifests = []string{ + "manifest/%s/target-container.yaml", + "manifest/%s/instance-container.yaml", + "manifest/%s/solution-container.yaml", "manifest/%s/target.yaml", "manifest/%s/solution.yaml", "manifest/%s/instance.yaml", diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-container.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-container.yaml new file mode 100644 index 000000000..acaa2c994 --- /dev/null +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: InstanceContainer +metadata: + name: instance03 +spec: diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance.yaml index c82578dd4..1c983e403 100644 --- a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance.yaml +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance.yaml @@ -3,6 +3,7 @@ kind: Instance metadata: name: instance03-v1 spec: + rootResource: instance03 scope: k8s-scope solution: solution03:v1 target: diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution-container.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution-container.yaml new file mode 100644 index 000000000..934e31d32 --- /dev/null +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: SolutionContainer +metadata: + name: solution03 +spec: diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution.yaml index 9d40631ba..0352f325f 100644 --- a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution.yaml +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution.yaml @@ -2,7 +2,8 @@ apiVersion: solution.symphony/v1 kind: Solution metadata: name: solution03-v1 -spec: +spec: + rootResource: solution03 metadata: deployment.replicas: "#1" service.ports: "[{\"name\":\"port9090\",\"port\": 9090}]" diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-container.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-container.yaml new file mode 100644 index 000000000..34d44d1a3 --- /dev/null +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-container.yaml @@ -0,0 +1,5 @@ +apiVersion: fabric.symphony/v1 +kind: TargetContainer +metadata: + name: target03 +spec: diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target.yaml index 9e55dec64..c6b2504c3 100644 --- a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target.yaml +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target.yaml @@ -3,6 +3,7 @@ kind: Target metadata: name: target03-v1 spec: + rootResource: target03 forceRedeploy: true topologies: - bindings: diff --git a/test/integration/scenarios/04.workflow/magefile.go b/test/integration/scenarios/04.workflow/magefile.go index 8a39afc4f..e5f9925a7 100644 --- a/test/integration/scenarios/04.workflow/magefile.go +++ b/test/integration/scenarios/04.workflow/magefile.go @@ -35,6 +35,11 @@ var ( var ( // catalogs to deploy testCatalogs = []string{ + "test/integration/scenarios/04.workflow/manifest/catalog-catalog-container.yaml", + "test/integration/scenarios/04.workflow/manifest/instance-catalog-container.yaml", + "test/integration/scenarios/04.workflow/manifest/solution-catalog-container.yaml", + "test/integration/scenarios/04.workflow/manifest/target-catalog-container.yaml", + "test/integration/scenarios/04.workflow/manifest/catalog-catalog.yaml", "test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml", "test/integration/scenarios/04.workflow/manifest/solution-catalog.yaml", @@ -42,6 +47,7 @@ var ( } testCampaign = []string{ + "test/integration/scenarios/04.workflow/manifest/campaign-container.yaml", "test/integration/scenarios/04.workflow/manifest/campaign.yaml", } diff --git a/test/integration/scenarios/04.workflow/manifest/campaign-container.yaml b/test/integration/scenarios/04.workflow/manifest/campaign-container.yaml new file mode 100644 index 000000000..db6640d0f --- /dev/null +++ b/test/integration/scenarios/04.workflow/manifest/campaign-container.yaml @@ -0,0 +1,5 @@ +apiVersion: workflow.symphony/v1 +kind: CampaignContainer +metadata: + name: 04campaign +spec: diff --git a/test/integration/scenarios/04.workflow/manifest/campaign.yaml b/test/integration/scenarios/04.workflow/manifest/campaign.yaml index c37bab654..a19bfdda6 100644 --- a/test/integration/scenarios/04.workflow/manifest/campaign.yaml +++ b/test/integration/scenarios/04.workflow/manifest/campaign.yaml @@ -3,6 +3,7 @@ kind: Campaign metadata: name: 04campaign-v1 spec: + rootResource: 04campaign firstStage: wait stages: wait: @@ -16,10 +17,10 @@ spec: inputs: objectType: catalogs names: - - site-catalog:v1 - - site-app:v1 - - site-k8s-target:v1 - - site-instance:v1 + - sitecatalog:v1 + - siteapp:v1 + - sitek8starget:v1 + - siteinstance:v1 list: name: list provider: providers.stage.list @@ -35,7 +36,6 @@ spec: name: deploy provider: providers.stage.materialize stageSelector: "" - schedule: "2020-10-31T12:00:00-07:00" config: baseUrl: http://symphony-service:8080/v1alpha2/ user: admin diff --git a/test/integration/scenarios/04.workflow/manifest/catalog-catalog-container.yaml b/test/integration/scenarios/04.workflow/manifest/catalog-catalog-container.yaml new file mode 100644 index 000000000..5ffa8ecb0 --- /dev/null +++ b/test/integration/scenarios/04.workflow/manifest/catalog-catalog-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: sitecatalog +spec: diff --git a/test/integration/scenarios/04.workflow/manifest/catalog-catalog.yaml b/test/integration/scenarios/04.workflow/manifest/catalog-catalog.yaml index fd9d192c2..ba70f7a34 100644 --- a/test/integration/scenarios/04.workflow/manifest/catalog-catalog.yaml +++ b/test/integration/scenarios/04.workflow/manifest/catalog-catalog.yaml @@ -1,12 +1,13 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: - name: site-catalog-v1 + name: sitecatalog-v1 spec: + rootResource: sitecatalog catalogType: catalog properties: metadata: - name: web-app-config-v1 + name: webappconfig:v1 spec: type: config properties: diff --git a/test/integration/scenarios/04.workflow/manifest/instance-catalog-container.yaml b/test/integration/scenarios/04.workflow/manifest/instance-catalog-container.yaml new file mode 100644 index 000000000..c20f7fdab --- /dev/null +++ b/test/integration/scenarios/04.workflow/manifest/instance-catalog-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: siteinstance +spec: diff --git a/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml b/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml index b69bcbd24..87d98dd9e 100644 --- a/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml +++ b/test/integration/scenarios/04.workflow/manifest/instance-catalog.yaml @@ -1,12 +1,15 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: - name: site-instance-v1 + name: siteinstance-v1 spec: + rootResource: siteinstance catalogType: instance properties: + metadata: + name: siteinstance:v1 spec: - solution: site-app:v1 + solution: siteapp:v1 scope: SCOPENAME target: selector: diff --git a/test/integration/scenarios/04.workflow/manifest/solution-catalog-container.yaml b/test/integration/scenarios/04.workflow/manifest/solution-catalog-container.yaml new file mode 100644 index 000000000..97e180af3 --- /dev/null +++ b/test/integration/scenarios/04.workflow/manifest/solution-catalog-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: siteapp +spec: diff --git a/test/integration/scenarios/04.workflow/manifest/solution-catalog.yaml b/test/integration/scenarios/04.workflow/manifest/solution-catalog.yaml index bb683683d..734794aaa 100644 --- a/test/integration/scenarios/04.workflow/manifest/solution-catalog.yaml +++ b/test/integration/scenarios/04.workflow/manifest/solution-catalog.yaml @@ -1,18 +1,21 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: - name: site-app-v1 + name: siteapp-v1 spec: + rootResource: siteapp catalogType: solution properties: + metadata: + name: siteapp:v1 spec: components: - name: web-app type: container metadata: service.ports: "[{\"name\":\"port3011\",\"port\": 3011,\"targetPort\":5000}]" - service.type: "${{$config('web-app-config:v1','serviceType')}}" + service.type: "${{$config('webappconfig:v1','serviceType')}}" properties: deployment.replicas: "#1" container.ports: "[{\"containerPort\":5000,\"protocol\":\"TCP\"}]" - container.image: "${{$config('web-app-config:v1','image')}}" \ No newline at end of file + container.image: "${{$config('webappconfig:v1','image')}}" diff --git a/test/integration/scenarios/04.workflow/manifest/target-catalog-container.yaml b/test/integration/scenarios/04.workflow/manifest/target-catalog-container.yaml new file mode 100644 index 000000000..63a1851f3 --- /dev/null +++ b/test/integration/scenarios/04.workflow/manifest/target-catalog-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: sitek8starget +spec: diff --git a/test/integration/scenarios/04.workflow/manifest/target-catalog.yaml b/test/integration/scenarios/04.workflow/manifest/target-catalog.yaml index 419cb1902..59b7de3d4 100644 --- a/test/integration/scenarios/04.workflow/manifest/target-catalog.yaml +++ b/test/integration/scenarios/04.workflow/manifest/target-catalog.yaml @@ -1,10 +1,13 @@ apiVersion: federation.symphony/v1 kind: Catalog metadata: - name: site-k8s-target-v1 -spec: + name: sitek8starget-v1 +spec: + rootResource: sitek8starget catalogType: target properties: + metadata: + name: sitek8starget:v1 spec: properties: group: site diff --git a/test/integration/scenarios/05.catalog/catalogs/asset-container.yaml b/test/integration/scenarios/05.catalog/catalogs/asset-container.yaml new file mode 100644 index 000000000..d6f2aa4ea --- /dev/null +++ b/test/integration/scenarios/05.catalog/catalogs/asset-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: asset +spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/asset.yaml b/test/integration/scenarios/05.catalog/catalogs/asset.yaml index e22a0a3c0..8b0b2cdbd 100644 --- a/test/integration/scenarios/05.catalog/catalogs/asset.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/asset.yaml @@ -3,6 +3,7 @@ kind: Catalog metadata: name: asset-v1 spec: + rootResource: asset catalogType: asset properties: name: "東京" diff --git a/test/integration/scenarios/05.catalog/catalogs/config-container.yaml b/test/integration/scenarios/05.catalog/catalogs/config-container.yaml new file mode 100644 index 000000000..74df3d875 --- /dev/null +++ b/test/integration/scenarios/05.catalog/catalogs/config-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: config +spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/config.yaml b/test/integration/scenarios/05.catalog/catalogs/config.yaml index d79ba20cf..32c531041 100644 --- a/test/integration/scenarios/05.catalog/catalogs/config.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/config.yaml @@ -3,6 +3,7 @@ kind: Catalog metadata: name: config-v1 spec: + rootResource: config catalogType: config metadata: schema: schema-v1 diff --git a/test/integration/scenarios/05.catalog/catalogs/instance-container.yaml b/test/integration/scenarios/05.catalog/catalogs/instance-container.yaml new file mode 100644 index 000000000..07d792515 --- /dev/null +++ b/test/integration/scenarios/05.catalog/catalogs/instance-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: instance +spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/instance.yaml b/test/integration/scenarios/05.catalog/catalogs/instance.yaml index 38a69b92e..e05fa705b 100644 --- a/test/integration/scenarios/05.catalog/catalogs/instance.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/instance.yaml @@ -3,8 +3,11 @@ kind: Catalog metadata: name: instance-v1 spec: + rootResource: instance catalogType: instance properties: + metadata: + name: instance:v1 spec: solution: app:v1 target: diff --git a/test/integration/scenarios/05.catalog/catalogs/schema-container.yaml b/test/integration/scenarios/05.catalog/catalogs/schema-container.yaml new file mode 100644 index 000000000..6e6f97865 --- /dev/null +++ b/test/integration/scenarios/05.catalog/catalogs/schema-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: schema +spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/schema.yaml b/test/integration/scenarios/05.catalog/catalogs/schema.yaml index 0ed34ca49..ca725a2c9 100644 --- a/test/integration/scenarios/05.catalog/catalogs/schema.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/schema.yaml @@ -3,6 +3,7 @@ kind: Catalog metadata: name: schema-v1 spec: + rootResource: schema catalogType: schema properties: spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/solution-container.yaml b/test/integration/scenarios/05.catalog/catalogs/solution-container.yaml new file mode 100644 index 000000000..e1ee8fd0f --- /dev/null +++ b/test/integration/scenarios/05.catalog/catalogs/solution-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: solution +spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/solution.yaml b/test/integration/scenarios/05.catalog/catalogs/solution.yaml index 66e359df0..e0026a89f 100644 --- a/test/integration/scenarios/05.catalog/catalogs/solution.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/solution.yaml @@ -3,8 +3,11 @@ kind: Catalog metadata: name: solution-v1 spec: + rootResource: solution catalogType: solution properties: + metadata: + name: solution:v1 spec: displayName: site-app-v1 components: diff --git a/test/integration/scenarios/05.catalog/catalogs/target-container.yaml b/test/integration/scenarios/05.catalog/catalogs/target-container.yaml new file mode 100644 index 000000000..de5edc924 --- /dev/null +++ b/test/integration/scenarios/05.catalog/catalogs/target-container.yaml @@ -0,0 +1,5 @@ +apiVersion: federation.symphony/v1 +kind: CatalogContainer +metadata: + name: target +spec: diff --git a/test/integration/scenarios/05.catalog/catalogs/target.yaml b/test/integration/scenarios/05.catalog/catalogs/target.yaml index 23289ff78..d6d228885 100644 --- a/test/integration/scenarios/05.catalog/catalogs/target.yaml +++ b/test/integration/scenarios/05.catalog/catalogs/target.yaml @@ -3,8 +3,11 @@ kind: Catalog metadata: name: target-v1 spec: + rootResource: target catalogType: target properties: + metadata: + name: target:v1 spec: properties: group: site diff --git a/test/integration/scenarios/05.catalog/magefile.go b/test/integration/scenarios/05.catalog/magefile.go index a907b2723..59744b950 100644 --- a/test/integration/scenarios/05.catalog/magefile.go +++ b/test/integration/scenarios/05.catalog/magefile.go @@ -35,6 +35,13 @@ const ( var ( // catalogs to deploy testCatalogs = []string{ + "catalogs/instance-container.yaml", + "catalogs/solution-container.yaml", + "catalogs/target-container.yaml", + "catalogs/asset-container.yaml", + "catalogs/config-container.yaml", + "catalogs/schema-container.yaml", + "catalogs/instance.yaml", "catalogs/solution.yaml", "catalogs/target.yaml", diff --git a/test/integration/scenarios/06.ado/create_update_fallback_test.go b/test/integration/scenarios/06.ado/create_update_fallback_test.go index 17e9acdaf..7080840b6 100644 --- a/test/integration/scenarios/06.ado/create_update_fallback_test.go +++ b/test/integration/scenarios/06.ado/create_update_fallback_test.go @@ -28,6 +28,9 @@ var _ = Describe("Create/update resources for rollback testing", Ordered, func() var targetBytes []byte var solutionBytes []byte var solutionBytesV2 []byte + var instanceContainerBytes []byte + var targetContainerBytes []byte + var solutionContainerBytes []byte var targetProps map[string]string BeforeAll(func(ctx context.Context) { @@ -52,9 +55,27 @@ var _ = Describe("Create/update resources for rollback testing", Ordered, func() }) runner := func(ctx context.Context, testcase TestCase) { - By("setting the components for the target") var err error + By("deploy solution container") + solutionContainerBytes, err = testhelpers.PatchSolutionContainer(defaultSolutionContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploy target container") + targetContainerBytes, err = testhelpers.PatchTargetContainer(defaultTargetContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", targetContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploy instance container") + instanceContainerBytes, err = testhelpers.PatchInstanceContainer(defaultInstanceContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for the target") props := targetProps if testcase.TargetProperties != nil { props = testcase.TargetProperties diff --git a/test/integration/scenarios/06.ado/create_update_test.go b/test/integration/scenarios/06.ado/create_update_test.go index c40d270e1..455ee7a64 100644 --- a/test/integration/scenarios/06.ado/create_update_test.go +++ b/test/integration/scenarios/06.ado/create_update_test.go @@ -30,6 +30,9 @@ var _ = Describe("Create resources with sequential changes", Ordered, func() { var instanceBytes []byte var targetBytes []byte var solutionBytes []byte + var instanceContainerBytes []byte + var targetContainerBytes []byte + var solutionContainerBytes []byte var specTimeout = 120 * time.Second var targetProps map[string]string var instanceParams map[string]interface{} @@ -56,8 +59,27 @@ var _ = Describe("Create resources with sequential changes", Ordered, func() { }) runner := func(ctx context.Context, testcase TestCase) { - By("setting the components for the target") var err error + + By("deploy solution container") + solutionContainerBytes, err = testhelpers.PatchSolutionContainer(defaultSolutionContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploy target container") + targetContainerBytes, err = testhelpers.PatchTargetContainer(defaultTargetContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", targetContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploy instance container") + instanceContainerBytes, err = testhelpers.PatchInstanceContainer(defaultInstanceContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for the target") props := targetProps params := instanceParams if testcase.TargetProperties != nil { diff --git a/test/integration/scenarios/06.ado/delete_test.go b/test/integration/scenarios/06.ado/delete_test.go index 75c29300f..79b295ec5 100644 --- a/test/integration/scenarios/06.ado/delete_test.go +++ b/test/integration/scenarios/06.ado/delete_test.go @@ -19,6 +19,9 @@ var _ = Describe("Delete", Ordered, func() { var instanceBytes []byte var targetBytes []byte var solutionBytes []byte + var instanceContainerBytes []byte + var targetContainerBytes []byte + var solutionContainerBytes []byte var specTimeout = 2 * time.Minute type DeleteTestCase struct { @@ -53,8 +56,27 @@ var _ = Describe("Delete", Ordered, func() { DescribeTable("when performing create/update operations", Ordered, func(ctx context.Context, testcase DeleteTestCase) { - By("setting the components for the target") var err error + + By("deploy solution container") + solutionContainerBytes, err = testhelpers.PatchSolutionContainer(defaultSolutionContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploy target container") + targetContainerBytes, err = testhelpers.PatchTargetContainer(defaultTargetContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", targetContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploy instance container") + instanceContainerBytes, err = testhelpers.PatchInstanceContainer(defaultInstanceContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for the target") targetBytes, err = testhelpers.PatchTarget(defaultTargetManifest, testhelpers.TargetOptions{ ComponentNames: testcase.TargetComponents, }) diff --git a/test/integration/scenarios/06.ado/manifest/instance-container.yaml b/test/integration/scenarios/06.ado/manifest/instance-container.yaml new file mode 100644 index 000000000..b39fde4e6 --- /dev/null +++ b/test/integration/scenarios/06.ado/manifest/instance-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: InstanceContainer +metadata: + name: instance +spec: diff --git a/test/integration/scenarios/06.ado/manifest/instance.yaml b/test/integration/scenarios/06.ado/manifest/instance.yaml index 3a4664e13..3c9785016 100644 --- a/test/integration/scenarios/06.ado/manifest/instance.yaml +++ b/test/integration/scenarios/06.ado/manifest/instance.yaml @@ -3,6 +3,7 @@ kind: Instance metadata: name: instance-v1 spec: + rootResource: instance target: name: target:v1 solution: solution:v1 diff --git a/test/integration/scenarios/06.ado/manifest/solution-container.yaml b/test/integration/scenarios/06.ado/manifest/solution-container.yaml new file mode 100644 index 000000000..a57bd3d21 --- /dev/null +++ b/test/integration/scenarios/06.ado/manifest/solution-container.yaml @@ -0,0 +1,5 @@ +apiVersion: solution.symphony/v1 +kind: SolutionContainer +metadata: + name: solution +spec: diff --git a/test/integration/scenarios/06.ado/manifest/solution.yaml b/test/integration/scenarios/06.ado/manifest/solution.yaml index 9f08c5cad..ce3c14890 100644 --- a/test/integration/scenarios/06.ado/manifest/solution.yaml +++ b/test/integration/scenarios/06.ado/manifest/solution.yaml @@ -3,4 +3,5 @@ kind: Solution metadata: name: solution-v1 spec: + rootResource: solution components: [] \ No newline at end of file diff --git a/test/integration/scenarios/06.ado/manifest/target-container.yaml b/test/integration/scenarios/06.ado/manifest/target-container.yaml new file mode 100644 index 000000000..225b43e66 --- /dev/null +++ b/test/integration/scenarios/06.ado/manifest/target-container.yaml @@ -0,0 +1,5 @@ +apiVersion: fabric.symphony/v1 +kind: TargetContainer +metadata: + name: target +spec: diff --git a/test/integration/scenarios/06.ado/manifest/target.yaml b/test/integration/scenarios/06.ado/manifest/target.yaml index f50475837..7aecc30b4 100644 --- a/test/integration/scenarios/06.ado/manifest/target.yaml +++ b/test/integration/scenarios/06.ado/manifest/target.yaml @@ -3,6 +3,7 @@ kind: Target metadata: name: target-v1 spec: + rootResource: target scope: azure-iot-operations components: [] topologies: diff --git a/test/integration/scenarios/06.ado/rbac_test.go b/test/integration/scenarios/06.ado/rbac_test.go index 2ea806f81..269b61256 100644 --- a/test/integration/scenarios/06.ado/rbac_test.go +++ b/test/integration/scenarios/06.ado/rbac_test.go @@ -30,11 +30,33 @@ var _ = Describe("RBAC", Ordered, func() { var instanceBytes []byte var targetBytes []byte var solutionBytes []byte + var instanceContainerBytes []byte + var targetContainerBytes []byte + var solutionContainerBytes []byte var specTimeout = 3 * time.Minute var installValues HelmValues var runRbacTest = func(ctx context.Context, testcase Rbac) { - By("setting the components for the target and scope") var err error + + By("deploy solution container") + solutionContainerBytes, err = testhelpers.PatchSolutionContainer(defaultSolutionContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", solutionContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploy target container") + targetContainerBytes, err = testhelpers.PatchTargetContainer(defaultTargetContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", targetContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("deploy instance container") + instanceContainerBytes, err = testhelpers.PatchInstanceContainer(defaultInstanceContainerManifest, testhelpers.InstanceOptions{}) + Expect(err).ToNot(HaveOccurred()) + err = shell.PipeInExec(ctx, "kubectl apply -f -", instanceContainerBytes) + Expect(err).ToNot(HaveOccurred()) + + By("setting the components for the target and scope") targetBytes, err = testhelpers.PatchTarget(defaultTargetManifest, testhelpers.TargetOptions{ ComponentNames: testcase.TargetComponents, Scope: testcase.TargetScope, diff --git a/test/integration/scenarios/06.ado/suite_test.go b/test/integration/scenarios/06.ado/suite_test.go index 43688d565..45265e4c4 100644 --- a/test/integration/scenarios/06.ado/suite_test.go +++ b/test/integration/scenarios/06.ado/suite_test.go @@ -24,6 +24,15 @@ var defaultTargetManifest []byte //go:embed manifest/solution.yaml var defaultSolutionManifest []byte +//go:embed manifest/instance-container.yaml +var defaultInstanceContainerManifest []byte + +//go:embed manifest/target-container.yaml +var defaultTargetContainerManifest []byte + +//go:embed manifest/solution-container.yaml +var defaultSolutionContainerManifest []byte + var successfullTargetExpectation = kube.Must(kube.Target("target-v1", "default", kube.WithCondition(conditions.All( kube.ProvisioningSucceededCondition, //kube.OperationIdMatchCondition, From f05ef66408d261f92c083ab5d65e1a627d338523 Mon Sep 17 00:00:00 2001 From: Lency Qian Date: Thu, 6 Jun 2024 23:18:51 +0800 Subject: [PATCH 2/6] container mvp --- api/constants/constants.go | 1 + .../campaign-container-manager.go | 181 ++++++++++++ .../campaign-container-manager_test.go | 38 +++ .../managers/campaigns/campaigns-manager.go | 48 ---- .../catalog-container-manager.go | 181 ++++++++++++ .../catalog-container-manager_test.go | 38 +++ .../managers/catalogs/catalogs-manager.go | 45 --- .../instance-container-manager.go | 181 ++++++++++++ .../instance-container-manager_test.go | 38 +++ .../managers/instances/instances-manager.go | 53 ---- .../apis/v1alpha1/managers/managerfactory.go | 15 + .../solution-container-manager.go | 181 ++++++++++++ .../solution-container-manager_test.go | 38 +++ .../managers/solutions/solutions-manager.go | 54 +--- .../target-container-manager.go | 181 ++++++++++++ .../target-container-manager_test.go | 38 +++ .../managers/targets/targets-manager.go | 48 ---- .../stage/materialize/materialize.go | 54 ++++ api/pkg/apis/v1alpha1/utils/apiclient.go | 260 ++++++++++++++++++ .../vendors/campaigns-container-vendor.go | 163 +++++++++++ .../campaigns-container-vendor_test.go | 142 ++++++++++ .../vendors/catalogs-container-vendor.go | 163 +++++++++++ .../vendors/catalogs-container-vendor_test.go | 142 ++++++++++ .../vendors/instances-container-vendor.go | 163 +++++++++++ .../instances-container-vendor_test.go | 142 ++++++++++ .../vendors/solutions-container-vendor.go | 163 +++++++++++ .../solutions-container-vendor_test.go | 142 ++++++++++ .../vendors/targets-container-vendor.go | 163 +++++++++++ .../vendors/targets-container-vendor_test.go | 142 ++++++++++ .../apis/v1alpha1/vendors/vendorfactory.go | 10 + api/symphony-api-dev-console-trace.json | 66 +++++ api/symphony-api-dev-zipkin-trace.json | 66 +++++ api/symphony-api-dev.json | 66 +++++ api/symphony-api-no-k8s-munchen.json | 96 +++++++ api/symphony-api-no-k8s-new-york.json | 96 +++++++ api/symphony-api-no-k8s-tokyo.json | 96 +++++++ api/symphony-api-no-k8s.json | 96 +++++++ api/symphony-api-production.json | 106 +++++++ api/symphony-api.json | 228 +++++++++++++-- .../helm/symphony/files/symphony-api.json | 106 +++++++ 40 files changed, 3965 insertions(+), 265 deletions(-) create mode 100644 api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager.go create mode 100644 api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager_test.go create mode 100644 api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager.go create mode 100644 api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager_test.go create mode 100644 api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager.go create mode 100644 api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager_test.go create mode 100644 api/pkg/apis/v1alpha1/managers/solutioncontainers/solution-container-manager.go create mode 100644 api/pkg/apis/v1alpha1/managers/solutioncontainers/solution-container-manager_test.go create mode 100644 api/pkg/apis/v1alpha1/managers/targetcontainers/target-container-manager.go create mode 100644 api/pkg/apis/v1alpha1/managers/targetcontainers/target-container-manager_test.go create mode 100644 api/pkg/apis/v1alpha1/vendors/campaigns-container-vendor.go create mode 100644 api/pkg/apis/v1alpha1/vendors/campaigns-container-vendor_test.go create mode 100644 api/pkg/apis/v1alpha1/vendors/catalogs-container-vendor.go create mode 100644 api/pkg/apis/v1alpha1/vendors/catalogs-container-vendor_test.go create mode 100644 api/pkg/apis/v1alpha1/vendors/instances-container-vendor.go create mode 100644 api/pkg/apis/v1alpha1/vendors/instances-container-vendor_test.go create mode 100644 api/pkg/apis/v1alpha1/vendors/solutions-container-vendor.go create mode 100644 api/pkg/apis/v1alpha1/vendors/solutions-container-vendor_test.go create mode 100644 api/pkg/apis/v1alpha1/vendors/targets-container-vendor.go create mode 100644 api/pkg/apis/v1alpha1/vendors/targets-container-vendor_test.go diff --git a/api/constants/constants.go b/api/constants/constants.go index 78bfeb591..10975783d 100644 --- a/api/constants/constants.go +++ b/api/constants/constants.go @@ -24,6 +24,7 @@ const ( GroupPrefix = "symphony" ManagerMetaKey = GroupPrefix + "/managed-by" InstanceMetaKey = GroupPrefix + "/instance" + NotFound = "Not Found" ) // Environment variables keys diff --git a/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager.go b/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager.go new file mode 100644 index 000000000..fc520b33f --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager.go @@ -0,0 +1,181 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package campaigncontainers + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + + observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" +) + +var log = logger.NewLogger("coa.runtime") + +type CampaignContainersManager struct { + managers.Manager + StateProvider states.IStateProvider +} + +func (s *CampaignContainersManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error { + err := s.Manager.Init(context, config, providers) + if err != nil { + return err + } + stateprovider, err := managers.GetStateProvider(config, providers) + if err == nil { + s.StateProvider = stateprovider + } else { + return err + } + return nil +} + +func (t *CampaignContainersManager) DeleteState(ctx context.Context, name string, namespace string) error { + ctx, span := observability.StartSpan("CampaignContainersManager", ctx, &map[string]string{ + "method": "DeleteState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + err = t.StateProvider.Delete(ctx, states.DeleteRequest{ + ID: name, + Metadata: map[string]interface{}{ + "namespace": namespace, + "group": model.SolutionGroup, + "version": "v1", + "resource": "campaigncontainers", + "kind": "CampaignContainer", + }, + }) + return err +} + +func (t *CampaignContainersManager) UpsertState(ctx context.Context, name string, state model.CampaignContainerState) error { + ctx, span := observability.StartSpan("CampaignContainersManager", ctx, &map[string]string{ + "method": "UpsertState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + if state.ObjectMeta.Name != "" && state.ObjectMeta.Name != name { + return v1alpha2.NewCOAError(nil, fmt.Sprintf("Name in metadata (%s) does not match name in request (%s)", state.ObjectMeta.Name, name), v1alpha2.BadRequest) + } + state.ObjectMeta.FixNames(name) + + body := map[string]interface{}{ + "apiVersion": model.WorkflowGroup + "/v1", + "kind": "CampaignContainer", + "metadata": state.ObjectMeta, + "spec": state.Spec, + } + + upsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: name, + Body: body, + ETag: "", + }, + Metadata: map[string]interface{}{ + "namespace": state.ObjectMeta.Namespace, + "group": model.WorkflowGroup, + "version": "v1", + "resource": "campaigncontainers", + "kind": "CampaignContainer", + }, + } + _, err = t.StateProvider.Upsert(ctx, upsertRequest) + if err != nil { + return err + } + return nil +} + +func (t *CampaignContainersManager) ListState(ctx context.Context, namespace string) ([]model.CampaignContainerState, error) { + ctx, span := observability.StartSpan("CampaignContainersManager", ctx, &map[string]string{ + "method": "ListState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + listRequest := states.ListRequest{ + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": "campaigncontainers", + "namespace": namespace, + "kind": "CampaignContainer", + }, + } + var campaigncontainers []states.StateEntry + campaigncontainers, _, err = t.StateProvider.List(ctx, listRequest) + if err != nil { + return nil, err + } + ret := make([]model.CampaignContainerState, 0) + for _, t := range campaigncontainers { + var rt model.CampaignContainerState + rt, err = getCampaignContainerState(t.Body, t.ETag) + if err != nil { + return nil, err + } + ret = append(ret, rt) + } + return ret, nil +} + +func getCampaignContainerState(body interface{}, etag string) (model.CampaignContainerState, error) { + var CampaignContainerState model.CampaignContainerState + bytes, _ := json.Marshal(body) + err := json.Unmarshal(bytes, &CampaignContainerState) + if err != nil { + return model.CampaignContainerState{}, err + } + if CampaignContainerState.Spec == nil { + CampaignContainerState.Spec = &model.CampaignContainerSpec{} + } + return CampaignContainerState, nil +} + +func (t *CampaignContainersManager) GetState(ctx context.Context, id string, namespace string) (model.CampaignContainerState, error) { + ctx, span := observability.StartSpan("CampaignContainersManager", ctx, &map[string]string{ + "method": "GetSpec", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + getRequest := states.GetRequest{ + ID: id, + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": "campaigncontainers", + "namespace": namespace, + "kind": "CampaignContainer", + }, + } + var Campaign states.StateEntry + Campaign, err = t.StateProvider.Get(ctx, getRequest) + if err != nil { + return model.CampaignContainerState{}, err + } + var ret model.CampaignContainerState + ret, err = getCampaignContainerState(Campaign.Body, Campaign.ETag) + if err != nil { + return model.CampaignContainerState{}, err + } + return ret, nil +} diff --git a/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager_test.go b/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager_test.go new file mode 100644 index 000000000..6ee8f32b4 --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager_test.go @@ -0,0 +1,38 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package campaigncontainers + +import ( + "context" + "testing" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/stretchr/testify/assert" +) + +// write test case to create a CampaignSpec using the manager +func TestCreateGetDeleteCampaignContainersState(t *testing.T) { + stateProvider := &memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + manager := CampaignContainersManager{ + StateProvider: stateProvider, + } + err := manager.UpsertState(context.Background(), "test", model.CampaignContainerState{}) + assert.Nil(t, err) + spec, err := manager.GetState(context.Background(), "test", "default") + assert.Nil(t, err) + assert.Equal(t, "test", spec.ObjectMeta.Name) + specLists, err := manager.ListState(context.Background(), "default") + assert.Nil(t, err) + assert.Equal(t, 1, len(specLists)) + assert.Equal(t, "test", specLists[0].ObjectMeta.Name) + err = manager.DeleteState(context.Background(), "test", "default") + assert.Nil(t, err) + spec, err = manager.GetState(context.Background(), "test", "default") + assert.NotNil(t, err) +} diff --git a/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go b/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go index f1452649c..43eb89098 100644 --- a/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go +++ b/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go @@ -19,11 +19,8 @@ import ( observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" - "github.com/eclipse-symphony/symphony/coa/pkg/logger" ) -var log = logger.NewLogger("coa.runtime") - type CampaignsManager struct { managers.Manager StateProvider states.IStateProvider @@ -99,51 +96,6 @@ func (m *CampaignsManager) UpsertState(ctx context.Context, name string, state m } state.ObjectMeta.FixNames(name) - if state.Spec != nil { - rootResource := state.Spec.RootResource - if rootResource != "" { - log.Debugf(" M (Campaigns): campaign root resource: %s, campaign: %s", rootResource, name) - resourceName := "campaigncontainers" - kind := "CampaignContainer" - containerMetadata := map[string]interface{}{ - "version": "v1", - "group": model.WorkflowGroup, - "resource": resourceName, - "namespace": state.ObjectMeta.Namespace, - "kind": kind, - } - getRequest := states.GetRequest{ - ID: rootResource, - Metadata: containerMetadata, - } - _, err = m.StateProvider.Get(ctx, getRequest) - if err != nil { - log.Debugf(" M (Campaigns): get campaign container %s, err %v", rootResource, err) - cErr, ok := err.(v1alpha2.COAError) - if ok && cErr.State == v1alpha2.NotFound { - containerBody := map[string]interface{}{ - "apiVersion": model.WorkflowGroup + "/v1", - "kind": kind, - "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, - "spec": model.CampaignContainerSpec{}, - } - containerUpsertRequest := states.UpsertRequest{ - Value: states.StateEntry{ - ID: rootResource, - Body: containerBody, - }, - Metadata: containerMetadata, - } - _, err = m.StateProvider.Upsert(ctx, containerUpsertRequest) - if err != nil { - log.Errorf(" M (Campaigns): failed to create campaign container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) - return err - } - } - } - } - } - upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ ID: name, diff --git a/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager.go b/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager.go new file mode 100644 index 000000000..2c5b79ea4 --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager.go @@ -0,0 +1,181 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package catalogcontainers + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + + observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" +) + +var log = logger.NewLogger("coa.runtime") + +type CatalogContainersManager struct { + managers.Manager + StateProvider states.IStateProvider +} + +func (s *CatalogContainersManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error { + err := s.Manager.Init(context, config, providers) + if err != nil { + return err + } + stateprovider, err := managers.GetStateProvider(config, providers) + if err == nil { + s.StateProvider = stateprovider + } else { + return err + } + return nil +} + +func (t *CatalogContainersManager) DeleteState(ctx context.Context, name string, namespace string) error { + ctx, span := observability.StartSpan("CatalogContainersManager", ctx, &map[string]string{ + "method": "DeleteState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + err = t.StateProvider.Delete(ctx, states.DeleteRequest{ + ID: name, + Metadata: map[string]interface{}{ + "namespace": namespace, + "group": model.FederationGroup, + "version": "v1", + "resource": "catalogcontainers", + "kind": "CatalogContainer", + }, + }) + return err +} + +func (t *CatalogContainersManager) UpsertState(ctx context.Context, name string, state model.CatalogContainerState) error { + ctx, span := observability.StartSpan("CatalogContainersManager", ctx, &map[string]string{ + "method": "UpsertState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + if state.ObjectMeta.Name != "" && state.ObjectMeta.Name != name { + return v1alpha2.NewCOAError(nil, fmt.Sprintf("Name in metadata (%s) does not match name in request (%s)", state.ObjectMeta.Name, name), v1alpha2.BadRequest) + } + state.ObjectMeta.FixNames(name) + + body := map[string]interface{}{ + "apiVersion": model.FederationGroup + "/v1", + "kind": "CatalogContainer", + "metadata": state.ObjectMeta, + "spec": state.Spec, + } + + upsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: name, + Body: body, + ETag: "", + }, + Metadata: map[string]interface{}{ + "namespace": state.ObjectMeta.Namespace, + "group": model.FederationGroup, + "version": "v1", + "resource": "catalogcontainers", + "kind": "CatalogContainer", + }, + } + _, err = t.StateProvider.Upsert(ctx, upsertRequest) + if err != nil { + return err + } + return nil +} + +func (t *CatalogContainersManager) ListState(ctx context.Context, namespace string) ([]model.CatalogContainerState, error) { + ctx, span := observability.StartSpan("CatalogContainersManager", ctx, &map[string]string{ + "method": "ListState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + listRequest := states.ListRequest{ + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.FederationGroup, + "resource": "catalogcontainers", + "namespace": namespace, + "kind": "CatalogContainer", + }, + } + var catalogcontainers []states.StateEntry + catalogcontainers, _, err = t.StateProvider.List(ctx, listRequest) + if err != nil { + return nil, err + } + ret := make([]model.CatalogContainerState, 0) + for _, t := range catalogcontainers { + var rt model.CatalogContainerState + rt, err = getCatalogContainerState(t.Body, t.ETag) + if err != nil { + return nil, err + } + ret = append(ret, rt) + } + return ret, nil +} + +func getCatalogContainerState(body interface{}, etag string) (model.CatalogContainerState, error) { + var CatalogContainerState model.CatalogContainerState + bytes, _ := json.Marshal(body) + err := json.Unmarshal(bytes, &CatalogContainerState) + if err != nil { + return model.CatalogContainerState{}, err + } + if CatalogContainerState.Spec == nil { + CatalogContainerState.Spec = &model.CatalogContainerSpec{} + } + return CatalogContainerState, nil +} + +func (t *CatalogContainersManager) GetState(ctx context.Context, id string, namespace string) (model.CatalogContainerState, error) { + ctx, span := observability.StartSpan("CatalogContainersManager", ctx, &map[string]string{ + "method": "GetSpec", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + getRequest := states.GetRequest{ + ID: id, + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.FederationGroup, + "resource": "catalogcontainers", + "namespace": namespace, + "kind": "CatalogContainer", + }, + } + var Campaign states.StateEntry + Campaign, err = t.StateProvider.Get(ctx, getRequest) + if err != nil { + return model.CatalogContainerState{}, err + } + var ret model.CatalogContainerState + ret, err = getCatalogContainerState(Campaign.Body, Campaign.ETag) + if err != nil { + return model.CatalogContainerState{}, err + } + return ret, nil +} diff --git a/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager_test.go b/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager_test.go new file mode 100644 index 000000000..e5a61311c --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager_test.go @@ -0,0 +1,38 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package catalogcontainers + +import ( + "context" + "testing" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/stretchr/testify/assert" +) + +// write test case to create a CampaignSpec using the manager +func TestCreateGetDeleteCampaignContainersState(t *testing.T) { + stateProvider := &memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + manager := CatalogContainersManager{ + StateProvider: stateProvider, + } + err := manager.UpsertState(context.Background(), "test", model.CatalogContainerState{}) + assert.Nil(t, err) + spec, err := manager.GetState(context.Background(), "test", "default") + assert.Nil(t, err) + assert.Equal(t, "test", spec.ObjectMeta.Name) + specLists, err := manager.ListState(context.Background(), "default") + assert.Nil(t, err) + assert.Equal(t, 1, len(specLists)) + assert.Equal(t, "test", specLists[0].ObjectMeta.Name) + err = manager.DeleteState(context.Background(), "test", "default") + assert.Nil(t, err) + spec, err = manager.GetState(context.Background(), "test", "default") + assert.NotNil(t, err) +} diff --git a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go index 53677d2fc..6fe1a8c4b 100644 --- a/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go +++ b/api/pkg/apis/v1alpha1/managers/catalogs/catalogs-manager.go @@ -152,51 +152,6 @@ func (m *CatalogsManager) UpsertState(ctx context.Context, name string, state mo return err } - if state.Spec != nil { - rootResource := state.Spec.RootResource - if rootResource != "" { - log.Debugf(" M (Catalogs): catalog root resource: %s, catalog: %s", rootResource, name) - resourceName := "catalogcontainers" - kind := "CatalogContainer" - containerMetadata := map[string]interface{}{ - "version": "v1", - "group": model.FederationGroup, - "resource": resourceName, - "namespace": state.ObjectMeta.Namespace, - "kind": kind, - } - getRequest := states.GetRequest{ - ID: rootResource, - Metadata: containerMetadata, - } - _, err = m.StateProvider.Get(ctx, getRequest) - if err != nil { - log.Debugf(" M (Catalogs): get catalog container %s, err %v", rootResource, err) - cErr, ok := err.(v1alpha2.COAError) - if ok && cErr.State == v1alpha2.NotFound { - containerBody := map[string]interface{}{ - "apiVersion": model.FederationGroup + "/v1", - "kind": kind, - "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, - "spec": model.CatalogContainerSpec{}, - } - containerUpsertRequest := states.UpsertRequest{ - Value: states.StateEntry{ - ID: rootResource, - Body: containerBody, - }, - Metadata: containerMetadata, - } - _, err = m.StateProvider.Upsert(ctx, containerUpsertRequest) - if err != nil { - log.Errorf(" M (Catalogs): failed to create catalog container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) - return err - } - } - } - } - } - upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ ID: name, diff --git a/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager.go b/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager.go new file mode 100644 index 000000000..94ffa9b96 --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager.go @@ -0,0 +1,181 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package instancecontainers + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + + observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" +) + +var log = logger.NewLogger("coa.runtime") + +type InstanceContainersManager struct { + managers.Manager + StateProvider states.IStateProvider +} + +func (s *InstanceContainersManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error { + err := s.Manager.Init(context, config, providers) + if err != nil { + return err + } + stateprovider, err := managers.GetStateProvider(config, providers) + if err == nil { + s.StateProvider = stateprovider + } else { + return err + } + return nil +} + +func (t *InstanceContainersManager) DeleteState(ctx context.Context, name string, namespace string) error { + ctx, span := observability.StartSpan("InstanceContainersManager", ctx, &map[string]string{ + "method": "DeleteState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + err = t.StateProvider.Delete(ctx, states.DeleteRequest{ + ID: name, + Metadata: map[string]interface{}{ + "namespace": namespace, + "group": model.SolutionGroup, + "version": "v1", + "resource": "instancecontainers", + "kind": "InstanceContainer", + }, + }) + return err +} + +func (t *InstanceContainersManager) UpsertState(ctx context.Context, name string, state model.InstanceContainerState) error { + ctx, span := observability.StartSpan("InstanceContainersManager", ctx, &map[string]string{ + "method": "UpsertState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + if state.ObjectMeta.Name != "" && state.ObjectMeta.Name != name { + return v1alpha2.NewCOAError(nil, fmt.Sprintf("Name in metadata (%s) does not match name in request (%s)", state.ObjectMeta.Name, name), v1alpha2.BadRequest) + } + state.ObjectMeta.FixNames(name) + + body := map[string]interface{}{ + "apiVersion": model.SolutionGroup + "/v1", + "kind": "InstanceContainer", + "metadata": state.ObjectMeta, + "spec": state.Spec, + } + + upsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: name, + Body: body, + ETag: "", + }, + Metadata: map[string]interface{}{ + "namespace": state.ObjectMeta.Namespace, + "group": model.SolutionGroup, + "version": "v1", + "resource": "instancecontainers", + "kind": "InstanceContainer", + }, + } + _, err = t.StateProvider.Upsert(ctx, upsertRequest) + if err != nil { + return err + } + return nil +} + +func (t *InstanceContainersManager) ListState(ctx context.Context, namespace string) ([]model.InstanceContainerState, error) { + ctx, span := observability.StartSpan("InstanceContainersManager", ctx, &map[string]string{ + "method": "ListState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + listRequest := states.ListRequest{ + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": "instancecontainers", + "namespace": namespace, + "kind": "InstanceContainer", + }, + } + var instanceContainers []states.StateEntry + instanceContainers, _, err = t.StateProvider.List(ctx, listRequest) + if err != nil { + return nil, err + } + ret := make([]model.InstanceContainerState, 0) + for _, t := range instanceContainers { + var rt model.InstanceContainerState + rt, err = getInstanceContainerState(t.Body, t.ETag) + if err != nil { + return nil, err + } + ret = append(ret, rt) + } + return ret, nil +} + +func getInstanceContainerState(body interface{}, etag string) (model.InstanceContainerState, error) { + var InstanceContainerState model.InstanceContainerState + bytes, _ := json.Marshal(body) + err := json.Unmarshal(bytes, &InstanceContainerState) + if err != nil { + return model.InstanceContainerState{}, err + } + if InstanceContainerState.Spec == nil { + InstanceContainerState.Spec = &model.InstanceContainerSpec{} + } + return InstanceContainerState, nil +} + +func (t *InstanceContainersManager) GetState(ctx context.Context, id string, namespace string) (model.InstanceContainerState, error) { + ctx, span := observability.StartSpan("InstanceContainersManager", ctx, &map[string]string{ + "method": "GetSpec", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + getRequest := states.GetRequest{ + ID: id, + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": "instancecontainers", + "namespace": namespace, + "kind": "InstanceContainer", + }, + } + var instance states.StateEntry + instance, err = t.StateProvider.Get(ctx, getRequest) + if err != nil { + return model.InstanceContainerState{}, err + } + var ret model.InstanceContainerState + ret, err = getInstanceContainerState(instance.Body, instance.ETag) + if err != nil { + return model.InstanceContainerState{}, err + } + return ret, nil +} diff --git a/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager_test.go b/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager_test.go new file mode 100644 index 000000000..686f6da91 --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager_test.go @@ -0,0 +1,38 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package instancecontainers + +import ( + "context" + "testing" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/stretchr/testify/assert" +) + +// write test case to create a InstanceSpec using the manager +func TestCreateGetDeleteInstanceContainersState(t *testing.T) { + stateProvider := &memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + manager := InstanceContainersManager{ + StateProvider: stateProvider, + } + err := manager.UpsertState(context.Background(), "test", model.InstanceContainerState{}) + assert.Nil(t, err) + spec, err := manager.GetState(context.Background(), "test", "default") + assert.Nil(t, err) + assert.Equal(t, "test", spec.ObjectMeta.Name) + specLists, err := manager.ListState(context.Background(), "default") + assert.Nil(t, err) + assert.Equal(t, 1, len(specLists)) + assert.Equal(t, "test", specLists[0].ObjectMeta.Name) + err = manager.DeleteState(context.Background(), "test", "default") + assert.Nil(t, err) + spec, err = manager.GetState(context.Background(), "test", "default") + assert.NotNil(t, err) +} diff --git a/api/pkg/apis/v1alpha1/managers/instances/instances-manager.go b/api/pkg/apis/v1alpha1/managers/instances/instances-manager.go index f3261708e..9d3c2de37 100644 --- a/api/pkg/apis/v1alpha1/managers/instances/instances-manager.go +++ b/api/pkg/apis/v1alpha1/managers/instances/instances-manager.go @@ -17,14 +17,11 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" - "github.com/eclipse-symphony/symphony/coa/pkg/logger" observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" ) -var log = logger.NewLogger("coa.runtime") - type InstancesManager struct { managers.Manager StateProvider states.IStateProvider @@ -83,58 +80,8 @@ func (t *InstancesManager) UpsertState(ctx context.Context, name string, state m "spec": state.Spec, } generation := "" - if state.Spec != nil { generation = state.Spec.Generation - rootResource := state.Spec.RootResource - if rootResource != "" { - log.Debugf(" M (Instances): instance root resource: %s, instance: %s", rootResource, name) - resourceName := "instancecontainers" - kind := "InstanceContainer" - containerMetadata := map[string]interface{}{ - "version": "v1", - "group": model.SolutionGroup, - "resource": resourceName, - "namespace": state.ObjectMeta.Namespace, - "kind": kind, - } - getRequest := states.GetRequest{ - ID: rootResource, - Metadata: map[string]interface{}{ - "version": "v1", - "group": model.SolutionGroup, - "resource": resourceName, - "namespace": state.ObjectMeta.Namespace, - "kind": kind, - }, - } - _, err = t.StateProvider.Get(ctx, getRequest) - if err != nil { - coae := err.(v1alpha2.COAError) - log.Infof(" M (Instances): get instance container %s, err %v, namespace: %v", rootResource, err, coae.State) - cErr, ok := err.(v1alpha2.COAError) - if ok && cErr.State == v1alpha2.NotFound { - containerBody := map[string]interface{}{ - "apiVersion": model.SolutionGroup + "/v1", - "kind": kind, - "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, - "spec": model.InstanceContainerSpec{}, - } - containerUpsertRequest := states.UpsertRequest{ - Value: states.StateEntry{ - ID: rootResource, - Body: containerBody, - }, - Metadata: containerMetadata, - } - _, err = t.StateProvider.Upsert(ctx, containerUpsertRequest) - if err != nil { - log.Errorf(" M (Instances): failed to create instance container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) - return err - } - } - } - } } upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ diff --git a/api/pkg/apis/v1alpha1/managers/managerfactory.go b/api/pkg/apis/v1alpha1/managers/managerfactory.go index b5e7bdf8c..2485ad71e 100644 --- a/api/pkg/apis/v1alpha1/managers/managerfactory.go +++ b/api/pkg/apis/v1alpha1/managers/managerfactory.go @@ -8,10 +8,13 @@ package managers import ( "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/activations" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/campaigncontainers" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/campaigns" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/catalogcontainers" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/catalogs" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/configs" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/devices" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/instancecontainers" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/instances" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/jobs" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/models" @@ -19,11 +22,13 @@ import ( "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/sites" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/skills" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solution" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solutioncontainers" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solutions" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/stage" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/staging" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/sync" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/target" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/targetcontainers" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/targets" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/trails" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/users" @@ -53,20 +58,30 @@ func (c *SymphonyManagerFactory) CreateManager(config cm.ManagerConfig) (cm.IMan manager = &target.TargetManager{} case "managers.symphony.targets": manager = &targets.TargetsManager{} + case "managers.symphony.targetcontainers": + manager = &targetcontainers.TargetContainersManager{} case "managers.symphony.devices": manager = &devices.DevicesManager{} case "managers.symphony.solutions": manager = &solutions.SolutionsManager{} + case "managers.symphony.solutioncontainers": + manager = &solutioncontainers.SolutionContainersManager{} case "managers.symphony.instances": manager = &instances.InstancesManager{} + case "managers.symphony.instancecontainers": + manager = &instancecontainers.InstanceContainersManager{} case "managers.symphony.users": manager = &users.UsersManager{} case "managers.symphony.jobs": manager = &jobs.JobsManager{} case "managers.symphony.campaigns": manager = &campaigns.CampaignsManager{} + case "managers.symphony.campaigncontainers": + manager = &campaigncontainers.CampaignContainersManager{} case "managers.symphony.catalogs": manager = &catalogs.CatalogsManager{} + case "managers.symphony.catalogcontainers": + manager = &catalogcontainers.CatalogContainersManager{} case "managers.symphony.activations": manager = &activations.ActivationsManager{} case "managers.symphony.activationscleanup": diff --git a/api/pkg/apis/v1alpha1/managers/solutioncontainers/solution-container-manager.go b/api/pkg/apis/v1alpha1/managers/solutioncontainers/solution-container-manager.go new file mode 100644 index 000000000..a02a9211a --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/solutioncontainers/solution-container-manager.go @@ -0,0 +1,181 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package solutioncontainers + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + + observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" +) + +var log = logger.NewLogger("coa.runtime") + +type SolutionContainersManager struct { + managers.Manager + StateProvider states.IStateProvider +} + +func (s *SolutionContainersManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error { + err := s.Manager.Init(context, config, providers) + if err != nil { + return err + } + stateprovider, err := managers.GetStateProvider(config, providers) + if err == nil { + s.StateProvider = stateprovider + } else { + return err + } + return nil +} + +func (t *SolutionContainersManager) DeleteState(ctx context.Context, name string, namespace string) error { + ctx, span := observability.StartSpan("SolutionContainersManager", ctx, &map[string]string{ + "method": "DeleteState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + err = t.StateProvider.Delete(ctx, states.DeleteRequest{ + ID: name, + Metadata: map[string]interface{}{ + "namespace": namespace, + "group": model.SolutionGroup, + "version": "v1", + "resource": "solutioncontainers", + "kind": "SolutionContainer", + }, + }) + return err +} + +func (t *SolutionContainersManager) UpsertState(ctx context.Context, name string, state model.SolutionContainerState) error { + ctx, span := observability.StartSpan("SolutionContainersManager", ctx, &map[string]string{ + "method": "UpsertState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + if state.ObjectMeta.Name != "" && state.ObjectMeta.Name != name { + return v1alpha2.NewCOAError(nil, fmt.Sprintf("Name in metadata (%s) does not match name in request (%s)", state.ObjectMeta.Name, name), v1alpha2.BadRequest) + } + state.ObjectMeta.FixNames(name) + + body := map[string]interface{}{ + "apiVersion": model.SolutionGroup + "/v1", + "kind": "SolutionContainer", + "metadata": state.ObjectMeta, + "spec": state.Spec, + } + + upsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: name, + Body: body, + ETag: "", + }, + Metadata: map[string]interface{}{ + "namespace": state.ObjectMeta.Namespace, + "group": model.SolutionGroup, + "version": "v1", + "resource": "solutioncontainers", + "kind": "SolutionContainer", + }, + } + _, err = t.StateProvider.Upsert(ctx, upsertRequest) + if err != nil { + return err + } + return nil +} + +func (t *SolutionContainersManager) ListState(ctx context.Context, namespace string) ([]model.SolutionContainerState, error) { + ctx, span := observability.StartSpan("SolutionContainersManager", ctx, &map[string]string{ + "method": "ListState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + listRequest := states.ListRequest{ + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": "solutioncontainers", + "namespace": namespace, + "kind": "SolutionContainer", + }, + } + var solutioncontainers []states.StateEntry + solutioncontainers, _, err = t.StateProvider.List(ctx, listRequest) + if err != nil { + return nil, err + } + ret := make([]model.SolutionContainerState, 0) + for _, t := range solutioncontainers { + var rt model.SolutionContainerState + rt, err = getSolutionContainerState(t.Body, t.ETag) + if err != nil { + return nil, err + } + ret = append(ret, rt) + } + return ret, nil +} + +func getSolutionContainerState(body interface{}, etag string) (model.SolutionContainerState, error) { + var SolutionContainerState model.SolutionContainerState + bytes, _ := json.Marshal(body) + err := json.Unmarshal(bytes, &SolutionContainerState) + if err != nil { + return model.SolutionContainerState{}, err + } + if SolutionContainerState.Spec == nil { + SolutionContainerState.Spec = &model.SolutionContainerSpec{} + } + return SolutionContainerState, nil +} + +func (t *SolutionContainersManager) GetState(ctx context.Context, id string, namespace string) (model.SolutionContainerState, error) { + ctx, span := observability.StartSpan("SolutionContainersManager", ctx, &map[string]string{ + "method": "GetSpec", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + getRequest := states.GetRequest{ + ID: id, + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.SolutionGroup, + "resource": "solutioncontainers", + "namespace": namespace, + "kind": "SolutionContainer", + }, + } + var Solution states.StateEntry + Solution, err = t.StateProvider.Get(ctx, getRequest) + if err != nil { + return model.SolutionContainerState{}, err + } + var ret model.SolutionContainerState + ret, err = getSolutionContainerState(Solution.Body, Solution.ETag) + if err != nil { + return model.SolutionContainerState{}, err + } + return ret, nil +} diff --git a/api/pkg/apis/v1alpha1/managers/solutioncontainers/solution-container-manager_test.go b/api/pkg/apis/v1alpha1/managers/solutioncontainers/solution-container-manager_test.go new file mode 100644 index 000000000..18e8e8664 --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/solutioncontainers/solution-container-manager_test.go @@ -0,0 +1,38 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package solutioncontainers + +import ( + "context" + "testing" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/stretchr/testify/assert" +) + +// write test case to create a SolutionContainerSpec using the manager +func TestCreateGetDeleteSolutionContainersState(t *testing.T) { + stateProvider := &memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + manager := SolutionContainersManager{ + StateProvider: stateProvider, + } + err := manager.UpsertState(context.Background(), "test", model.SolutionContainerState{}) + assert.Nil(t, err) + spec, err := manager.GetState(context.Background(), "test", "default") + assert.Nil(t, err) + assert.Equal(t, "test", spec.ObjectMeta.Name) + specLists, err := manager.ListState(context.Background(), "default") + assert.Nil(t, err) + assert.Equal(t, 1, len(specLists)) + assert.Equal(t, "test", specLists[0].ObjectMeta.Name) + err = manager.DeleteState(context.Background(), "test", "default") + assert.Nil(t, err) + spec, err = manager.GetState(context.Background(), "test", "default") + assert.NotNil(t, err) +} diff --git a/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go b/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go index af491d66e..11b484e75 100644 --- a/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go +++ b/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go @@ -15,14 +15,12 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" - observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" - observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" - "github.com/eclipse-symphony/symphony/coa/pkg/logger" -) -var log = logger.NewLogger("coa.runtime") + observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" +) type SolutionsManager struct { managers.Manager @@ -81,52 +79,6 @@ func (t *SolutionsManager) UpsertState(ctx context.Context, name string, state m "metadata": state.ObjectMeta, "spec": state.Spec, } - - if state.Spec != nil { - rootResource := state.Spec.RootResource - if rootResource != "" { - log.Debugf(" M (Solutions): solution root resource: %s, solution: %s", rootResource, name) - resourceName := "solutioncontainers" - kind := "SolutionContainer" - containerMetadata := map[string]interface{}{ - "version": "v1", - "group": model.SolutionGroup, - "resource": resourceName, - "namespace": state.ObjectMeta.Namespace, - "kind": kind, - } - getRequest := states.GetRequest{ - ID: rootResource, - Metadata: containerMetadata, - } - _, err = t.StateProvider.Get(ctx, getRequest) - if err != nil { - log.Debugf(" M (Solutions): get solution container %s, err %v", rootResource, err) - cErr, ok := err.(v1alpha2.COAError) - if ok && cErr.State == v1alpha2.NotFound { - containerBody := map[string]interface{}{ - "apiVersion": model.SolutionGroup + "/v1", - "kind": kind, - "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, - "spec": model.SolutionContainerSpec{}, - } - containerUpsertRequest := states.UpsertRequest{ - Value: states.StateEntry{ - ID: rootResource, - Body: containerBody, - }, - Metadata: containerMetadata, - } - _, err = t.StateProvider.Upsert(ctx, containerUpsertRequest) - if err != nil { - log.Errorf(" M (Solutions): failed to create solution container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) - return err - } - } - } - } - } - upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ ID: name, diff --git a/api/pkg/apis/v1alpha1/managers/targetcontainers/target-container-manager.go b/api/pkg/apis/v1alpha1/managers/targetcontainers/target-container-manager.go new file mode 100644 index 000000000..aed38c47e --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/targetcontainers/target-container-manager.go @@ -0,0 +1,181 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package targetcontainers + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + + observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" +) + +var log = logger.NewLogger("coa.runtime") + +type TargetContainersManager struct { + managers.Manager + StateProvider states.IStateProvider +} + +func (s *TargetContainersManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error { + err := s.Manager.Init(context, config, providers) + if err != nil { + return err + } + stateprovider, err := managers.GetStateProvider(config, providers) + if err == nil { + s.StateProvider = stateprovider + } else { + return err + } + return nil +} + +func (t *TargetContainersManager) DeleteState(ctx context.Context, name string, namespace string) error { + ctx, span := observability.StartSpan("TargetContainersManager", ctx, &map[string]string{ + "method": "DeleteState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + err = t.StateProvider.Delete(ctx, states.DeleteRequest{ + ID: name, + Metadata: map[string]interface{}{ + "namespace": namespace, + "group": model.FabricGroup, + "version": "v1", + "resource": "targetcontainers", + "kind": "TargetContainer", + }, + }) + return err +} + +func (t *TargetContainersManager) UpsertState(ctx context.Context, name string, state model.TargetContainerState) error { + ctx, span := observability.StartSpan("TargetContainersManager", ctx, &map[string]string{ + "method": "UpsertState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + if state.ObjectMeta.Name != "" && state.ObjectMeta.Name != name { + return v1alpha2.NewCOAError(nil, fmt.Sprintf("Name in metadata (%s) does not match name in request (%s)", state.ObjectMeta.Name, name), v1alpha2.BadRequest) + } + state.ObjectMeta.FixNames(name) + + body := map[string]interface{}{ + "apiVersion": model.FabricGroup + "/v1", + "kind": "TargetContainer", + "metadata": state.ObjectMeta, + "spec": state.Spec, + } + + upsertRequest := states.UpsertRequest{ + Value: states.StateEntry{ + ID: name, + Body: body, + ETag: "", + }, + Metadata: map[string]interface{}{ + "namespace": state.ObjectMeta.Namespace, + "group": model.FabricGroup, + "version": "v1", + "resource": "targetcontainers", + "kind": "TargetContainer", + }, + } + _, err = t.StateProvider.Upsert(ctx, upsertRequest) + if err != nil { + return err + } + return nil +} + +func (t *TargetContainersManager) ListState(ctx context.Context, namespace string) ([]model.TargetContainerState, error) { + ctx, span := observability.StartSpan("TargetContainersManager", ctx, &map[string]string{ + "method": "ListState", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + listRequest := states.ListRequest{ + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.FabricGroup, + "resource": "targetcontainers", + "namespace": namespace, + "kind": "TargetContainer", + }, + } + var targetcontainers []states.StateEntry + targetcontainers, _, err = t.StateProvider.List(ctx, listRequest) + if err != nil { + return nil, err + } + ret := make([]model.TargetContainerState, 0) + for _, t := range targetcontainers { + var rt model.TargetContainerState + rt, err = getTargetContainerState(t.Body, t.ETag) + if err != nil { + return nil, err + } + ret = append(ret, rt) + } + return ret, nil +} + +func getTargetContainerState(body interface{}, etag string) (model.TargetContainerState, error) { + var TargetContainerState model.TargetContainerState + bytes, _ := json.Marshal(body) + err := json.Unmarshal(bytes, &TargetContainerState) + if err != nil { + return model.TargetContainerState{}, err + } + if TargetContainerState.Spec == nil { + TargetContainerState.Spec = &model.TargetContainerSpec{} + } + return TargetContainerState, nil +} + +func (t *TargetContainersManager) GetState(ctx context.Context, id string, namespace string) (model.TargetContainerState, error) { + ctx, span := observability.StartSpan("TargetContainersManager", ctx, &map[string]string{ + "method": "GetSpec", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + getRequest := states.GetRequest{ + ID: id, + Metadata: map[string]interface{}{ + "version": "v1", + "group": model.FabricGroup, + "resource": "targetcontainers", + "namespace": namespace, + "kind": "TargetContainer", + }, + } + var Target states.StateEntry + Target, err = t.StateProvider.Get(ctx, getRequest) + if err != nil { + return model.TargetContainerState{}, err + } + var ret model.TargetContainerState + ret, err = getTargetContainerState(Target.Body, Target.ETag) + if err != nil { + return model.TargetContainerState{}, err + } + return ret, nil +} diff --git a/api/pkg/apis/v1alpha1/managers/targetcontainers/target-container-manager_test.go b/api/pkg/apis/v1alpha1/managers/targetcontainers/target-container-manager_test.go new file mode 100644 index 000000000..48feb46b4 --- /dev/null +++ b/api/pkg/apis/v1alpha1/managers/targetcontainers/target-container-manager_test.go @@ -0,0 +1,38 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package targetcontainers + +import ( + "context" + "testing" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/stretchr/testify/assert" +) + +// write test case to create a TargetContainerSpec using the manager +func TestCreateGetDeleteTargetContainerState(t *testing.T) { + stateProvider := &memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + manager := TargetContainersManager{ + StateProvider: stateProvider, + } + err := manager.UpsertState(context.Background(), "test", model.TargetContainerState{}) + assert.Nil(t, err) + spec, err := manager.GetState(context.Background(), "test", "default") + assert.Nil(t, err) + assert.Equal(t, "test", spec.ObjectMeta.Name) + specLists, err := manager.ListState(context.Background(), "default") + assert.Nil(t, err) + assert.Equal(t, 1, len(specLists)) + assert.Equal(t, "test", specLists[0].ObjectMeta.Name) + err = manager.DeleteState(context.Background(), "test", "default") + assert.Nil(t, err) + spec, err = manager.GetState(context.Background(), "test", "default") + assert.NotNil(t, err) +} diff --git a/api/pkg/apis/v1alpha1/managers/targets/targets-manager.go b/api/pkg/apis/v1alpha1/managers/targets/targets-manager.go index 126fcfaf8..5b50b7318 100644 --- a/api/pkg/apis/v1alpha1/managers/targets/targets-manager.go +++ b/api/pkg/apis/v1alpha1/managers/targets/targets-manager.go @@ -19,13 +19,10 @@ import ( "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/registry" "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states" - "github.com/eclipse-symphony/symphony/coa/pkg/logger" observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" ) -var log = logger.NewLogger("coa.runtime") - type TargetsManager struct { managers.Manager StateProvider states.IStateProvider @@ -86,51 +83,6 @@ func (t *TargetsManager) UpsertState(ctx context.Context, name string, state mod "spec": state.Spec, } - if state.Spec != nil { - rootResource := state.Spec.RootResource - if rootResource != "" { - log.Debugf(" M (Targets): target root resource: %s, target: %s", rootResource, name) - resourceName := "targetcontainers" - kind := "TargetContainer" - containerMetadata := map[string]interface{}{ - "version": "v1", - "group": model.FabricGroup, - "resource": resourceName, - "namespace": state.ObjectMeta.Namespace, - "kind": kind, - } - getRequest := states.GetRequest{ - ID: rootResource, - Metadata: containerMetadata, - } - _, err = t.StateProvider.Get(ctx, getRequest) - if err != nil { - log.Debugf(" M (Targets): get target container %s, err %v", rootResource, err) - cErr, ok := err.(v1alpha2.COAError) - if ok && cErr.State == v1alpha2.NotFound { - containerBody := map[string]interface{}{ - "apiVersion": model.FabricGroup + "/v1", - "kind": kind, - "metadata": model.ObjectMeta{Namespace: state.ObjectMeta.Namespace, Name: rootResource}, - "spec": model.TargetContainerSpec{}, - } - containerUpsertRequest := states.UpsertRequest{ - Value: states.StateEntry{ - ID: rootResource, - Body: containerBody, - }, - Metadata: containerMetadata, - } - _, err = t.StateProvider.Upsert(ctx, containerUpsertRequest) - if err != nil { - log.Errorf(" M (Targets): failed to create target container %s, namespace: %v, err %v", rootResource, state.ObjectMeta.Namespace, err) - return err - } - } - } - } - } - upsertRequest := states.UpsertRequest{ Value: states.StateEntry{ ID: name, diff --git a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go index 352782a23..b9c4693da 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go +++ b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go @@ -13,6 +13,7 @@ import ( "strings" "sync" + "github.com/eclipse-symphony/symphony/api/constants" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" @@ -175,6 +176,19 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Instance name is invalid: catalog - %s", name), v1alpha2.BadRequest) } + mLog.Debugf(" P (Materialize Processor): check instance contains %v, namespace %s", instanceState.ObjectMeta.Name, instanceState.ObjectMeta.Namespace) + _, err := i.ApiClient.GetInstanceContainer(ctx, instanceState.Spec.RootResource, instanceState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + if err != nil && strings.Contains(err.Error(), constants.NotFound) { + mLog.Debugf("Failed to get instance container %s: %s", instanceState.Spec.RootResource, err.Error()) + instanceContainerState := model.InstanceContainerState{ObjectMeta: model.ObjectMeta{Name: instanceState.Spec.RootResource, Namespace: instanceState.ObjectMeta.Namespace}} + containerObjectData, _ := json.Marshal(instanceContainerState) + err = i.ApiClient.CreateInstanceContainer(ctx, instanceState.Spec.RootResource, containerObjectData, instanceState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + if err != nil { + mLog.Errorf("Failed to create instance container %s: %s", instanceState.Spec.RootResource, err.Error()) + return outputs, false, err + } + } + instanceState.ObjectMeta = updateObjectMeta(instanceState.ObjectMeta, inputs) objectData, _ := json.Marshal(instanceState) mLog.Debugf(" P (Materialize Processor): materialize instance %v to namespace %s", instanceState.ObjectMeta.Name, instanceState.ObjectMeta.Namespace) @@ -207,6 +221,20 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid solution name: catalog - %s", name), v1alpha2.BadRequest) } + mLog.Debugf(" P (Materialize Processor): check solution contains %v, namespace %s", solutionState.Spec.RootResource, solutionState.ObjectMeta.Namespace) + _, err := i.ApiClient.GetSolutionContainer(ctx, solutionState.Spec.RootResource, solutionState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + + if err != nil && strings.Contains(err.Error(), constants.NotFound) { + mLog.Debugf("Failed to get solution container %s: %s", solutionState.Spec.RootResource, err.Error()) + solutionContainerState := model.SolutionContainerState{ObjectMeta: model.ObjectMeta{Name: solutionState.Spec.RootResource, Namespace: solutionState.ObjectMeta.Namespace}} + containerObjectData, _ := json.Marshal(solutionContainerState) + err = i.ApiClient.CreateSolutionContainer(ctx, solutionState.Spec.RootResource, containerObjectData, solutionState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + if err != nil { + mLog.Errorf("Failed to create solution container %s: %s", solutionState.Spec.RootResource, err.Error()) + return outputs, false, err + } + } + solutionState.ObjectMeta = updateObjectMeta(solutionState.ObjectMeta, inputs) objectData, _ := json.Marshal(solutionState) mLog.Debugf(" P (Materialize Processor): materialize solution %v to namespace %s", solutionState.ObjectMeta.Name, solutionState.ObjectMeta.Namespace) @@ -239,6 +267,19 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid target name: %s", name), v1alpha2.BadRequest) } + mLog.Debugf(" P (Materialize Processor): check target contains %v, namespace %s", targetState.Spec.RootResource, targetState.ObjectMeta.Namespace) + _, err := i.ApiClient.GetTargetContainer(ctx, targetState.Spec.RootResource, targetState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + if err != nil && strings.Contains(err.Error(), constants.NotFound) { + mLog.Debugf("Failed to get target container %s: %s", targetState.Spec.RootResource, err.Error()) + targetContainerState := model.TargetContainerState{ObjectMeta: model.ObjectMeta{Name: targetState.Spec.RootResource, Namespace: targetState.ObjectMeta.Namespace}} + containerObjectData, _ := json.Marshal(targetContainerState) + err = i.ApiClient.CreateTargetContainer(ctx, targetState.Spec.RootResource, containerObjectData, targetState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + if err != nil { + mLog.Errorf("Failed to create target container %s: %s", targetState.Spec.RootResource, err.Error()) + return outputs, false, err + } + } + targetState.ObjectMeta = updateObjectMeta(targetState.ObjectMeta, inputs) objectData, _ := json.Marshal(targetState) mLog.Debugf(" P (Materialize Processor): materialize target %v to namespace %s", targetState.ObjectMeta.Name, targetState.ObjectMeta.Namespace) @@ -272,6 +313,19 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid catalog name: catalog - %s", name), v1alpha2.BadRequest) } + mLog.Debugf(" P (Materialize Processor): check catalog contains %v, namespace %s", catalogState.Spec.RootResource, catalogState.ObjectMeta.Namespace) + _, err := i.ApiClient.GetCatalogContainer(ctx, catalogState.Spec.RootResource, catalogState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + if err != nil && strings.Contains(err.Error(), constants.NotFound) { + mLog.Debugf("Failed to get catalog container %s: %s", catalogState.Spec.RootResource, err.Error()) + catalogContainerState := model.CatalogContainerState{ObjectMeta: model.ObjectMeta{Name: catalogState.Spec.RootResource, Namespace: catalogState.ObjectMeta.Namespace}} + containerObjectData, _ := json.Marshal(catalogContainerState) + err = i.ApiClient.CreateCatalogContainer(ctx, catalogState.Spec.RootResource, containerObjectData, catalogState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + if err != nil { + mLog.Errorf("Failed to create catalog container %s: %s", catalogState.Spec.RootResource, err.Error()) + return outputs, false, err + } + } + catalogState.ObjectMeta = updateObjectMeta(catalogState.ObjectMeta, inputs) objectData, _ := json.Marshal(catalogState) mLog.Debugf(" P (Materialize Processor): materialize catalog %v to namespace %s", catalogState.ObjectMeta.Name, catalogState.ObjectMeta.Namespace) diff --git a/api/pkg/apis/v1alpha1/utils/apiclient.go b/api/pkg/apis/v1alpha1/utils/apiclient.go index 78ff6179a..131a39b9f 100644 --- a/api/pkg/apis/v1alpha1/utils/apiclient.go +++ b/api/pkg/apis/v1alpha1/utils/apiclient.go @@ -82,6 +82,21 @@ type ( SyncActivationStatus(ctx context.Context, status model.ActivationStatus, user string, password string) error SendVisualizationPacket(ctx context.Context, payload []byte, user string, password string) error ReportCatalogs(ctx context.Context, instance string, components []model.ComponentSpec, user string, password string) error + CreateInstanceContainer(ctx context.Context, instanceContainer string, payload []byte, namespace string, user string, password string) error + DeleteInstanceContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) error + GetInstanceContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) (model.InstanceContainerState, error) + CreateSolutionContainer(ctx context.Context, instanceContainer string, payload []byte, namespace string, user string, password string) error + DeleteSolutionContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) error + GetSolutionContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) (model.SolutionContainerState, error) + CreateTargetContainer(ctx context.Context, instanceContainer string, payload []byte, namespace string, user string, password string) error + DeleteTargetContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) error + GetTargetContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) (model.TargetContainerState, error) + CreateCatalogContainer(ctx context.Context, instanceContainer string, payload []byte, namespace string, user string, password string) error + DeleteCatalogContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) error + GetCatalogContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) (model.CatalogContainerState, error) + CreateCampaignContainer(ctx context.Context, instanceContainer string, payload []byte, namespace string, user string, password string) error + DeleteCampaignContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) error + GetCampaignContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) (model.CampaignContainerState, error) } ) @@ -785,6 +800,251 @@ func (a *apiClient) callRestAPI(ctx context.Context, route string, method string return bodyBytes, nil } +func (a *apiClient) CreateInstanceContainer(ctx context.Context, instanceContainer string, payload []byte, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + //use proper url encoding in the following statement + _, err = a.callRestAPI(ctx, "instancecontainers/"+url.QueryEscape(instanceContainer)+"?namespace="+url.QueryEscape(namespace), "POST", payload, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) DeleteInstanceContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "instancecontainers/"+url.QueryEscape(instanceContainer)+"?direct=true&namespace="+url.QueryEscape(namespace), "DELETE", nil, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) GetInstanceContainer(ctx context.Context, instanceContainer string, namespace string, user string, password string) (model.InstanceContainerState, error) { + ret := model.InstanceContainerState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "instancecontainers/"+url.QueryEscape(instanceContainer)+"?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) CreateSolutionContainer(ctx context.Context, solutionContainer string, payload []byte, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "solutioncontainers/"+url.QueryEscape(solutionContainer)+"?namespace="+url.QueryEscape(namespace), "POST", payload, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) DeleteSolutionContainer(ctx context.Context, solutionContainer string, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "solutioncontainers/"+url.QueryEscape(solutionContainer)+"?direct=true&namespace="+url.QueryEscape(namespace), "DELETE", nil, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) GetSolutionContainer(ctx context.Context, solutionContainer string, namespace string, user string, password string) (model.SolutionContainerState, error) { + ret := model.SolutionContainerState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "solutioncontainers/"+url.QueryEscape(solutionContainer)+"?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) CreateTargetContainer(ctx context.Context, targetContainer string, payload []byte, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "targetcontainers/"+url.QueryEscape(targetContainer)+"?namespace="+url.QueryEscape(namespace), "POST", payload, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) DeleteTargetContainer(ctx context.Context, targetContainer string, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "targetcontainers/"+url.QueryEscape(targetContainer)+"?direct=true&namespace="+url.QueryEscape(namespace), "DELETE", nil, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) GetTargetContainer(ctx context.Context, targetContainer string, namespace string, user string, password string) (model.TargetContainerState, error) { + ret := model.TargetContainerState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "targetcontainers/"+url.QueryEscape(targetContainer)+"?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) CreateCatalogContainer(ctx context.Context, catalogContainer string, payload []byte, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "catalogcontainers/"+url.QueryEscape(catalogContainer)+"?namespace="+url.QueryEscape(namespace), "POST", payload, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) DeleteCatalogContainer(ctx context.Context, catalogContainer string, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "catalogcontainers/"+url.QueryEscape(catalogContainer)+"?direct=true&namespace="+url.QueryEscape(namespace), "DELETE", nil, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) GetCatalogContainer(ctx context.Context, catalogContainer string, namespace string, user string, password string) (model.CatalogContainerState, error) { + ret := model.CatalogContainerState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "catalogcontainers/"+url.QueryEscape(catalogContainer)+"?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + +func (a *apiClient) CreateCampaignContainer(ctx context.Context, campaignContainer string, payload []byte, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "campaigncontainers/"+url.QueryEscape(campaignContainer)+"?namespace="+url.QueryEscape(namespace), "POST", payload, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) DeleteCampaignContainer(ctx context.Context, campaignContainer string, namespace string, user string, password string) error { + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + if err != nil { + return err + } + + _, err = a.callRestAPI(ctx, "campaigncontainers/"+url.QueryEscape(campaignContainer)+"?direct=true&namespace="+url.QueryEscape(namespace), "DELETE", nil, token) + if err != nil { + return err + } + + return nil +} + +func (a *apiClient) GetCampaignContainer(ctx context.Context, campaignContainer string, namespace string, user string, password string) (model.CampaignContainerState, error) { + ret := model.CampaignContainerState{} + token, err := a.tokenProvider(ctx, a.baseUrl, a.client, user, password) + + if err != nil { + return ret, err + } + + response, err := a.callRestAPI(ctx, "campaigncontainers/"+url.QueryEscape(campaignContainer)+"?namespace="+url.QueryEscape(namespace), "GET", nil, token) + if err != nil { + return ret, err + } + + err = json.Unmarshal(response, &ret) + if err != nil { + return ret, err + } + + return ret, nil +} + func newHttpClient(ctx context.Context, secure bool) (*http.Client, error) { client := &http.Client{} if !secure { diff --git a/api/pkg/apis/v1alpha1/vendors/campaigns-container-vendor.go b/api/pkg/apis/v1alpha1/vendors/campaigns-container-vendor.go new file mode 100644 index 000000000..1045c142e --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/campaigns-container-vendor.go @@ -0,0 +1,163 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/campaigncontainers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + "github.com/valyala/fasthttp" +) + +var ccLog = logger.NewLogger("coa.runtime") + +type CampaignContainersVendor struct { + vendors.Vendor + CampaignContainersManager *campaigncontainers.CampaignContainersManager +} + +func (o *CampaignContainersVendor) GetInfo() vendors.VendorInfo { + return vendors.VendorInfo{ + Version: o.Vendor.Version, + Name: "CampaignContainers", + Producer: "Microsoft", + } +} + +func (e *CampaignContainersVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error { + err := e.Vendor.Init(config, factories, providers, pubsubProvider) + if err != nil { + return err + } + for _, m := range e.Managers { + if c, ok := m.(*campaigncontainers.CampaignContainersManager); ok { + e.CampaignContainersManager = c + } + } + if e.CampaignContainersManager == nil { + return v1alpha2.NewCOAError(nil, "Campaign container manager is not supplied", v1alpha2.MissingConfig) + } + return nil +} + +func (o *CampaignContainersVendor) GetEndpoints() []v1alpha2.Endpoint { + route := "campaigncontainers" + if o.Route != "" { + route = o.Route + } + return []v1alpha2.Endpoint{ + { + Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete}, + Route: route, + Version: o.Version, + Handler: o.onCampaignContainers, + Parameters: []string{"name?"}, + }, + } +} + +func (c *CampaignContainersVendor) onCampaignContainers(request v1alpha2.COARequest) v1alpha2.COAResponse { + pCtx, span := observability.StartSpan("onCampaignContainers", request.Context, &map[string]string{ + "method": "onCampaignContainers", + }) + defer span.End() + ccLog.Infof("V (CampaignContainers): onCampaignContainers, method: %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) + + id := request.Parameters["__name"] + namespace, exist := request.Parameters["namespace"] + if !exist { + namespace = constants.DefaultScope + } + + ccLog.Infof("V (CampaignContainers): onCampaignContainers, method: %s, traceId: %s", string(request.Method), span.SpanContext().TraceID().String()) + switch request.Method { + case fasthttp.MethodGet: + ctx, span := observability.StartSpan("onCampaignContainers-GET", pCtx, nil) + var err error + var state interface{} + isArray := false + if id == "" { + // Change partition back to empty to indicate ListSpec need to query all namespaces + if !exist { + namespace = "" + } + state, err = c.CampaignContainersManager.ListState(ctx, namespace) + isArray = true + } else { + state, err = c.CampaignContainersManager.GetState(ctx, id, namespace) + } + if err != nil { + ccLog.Errorf("V (CampaignContainers): onCampaignContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + jData, _ := utils.FormatObject(state, isArray, request.Parameters["path"], request.Parameters["doc-type"]) + resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + Body: jData, + ContentType: "application/json", + }) + if request.Parameters["doc-type"] == "yaml" { + resp.ContentType = "application/text" + } + return resp + case fasthttp.MethodPost: + ctx, span := observability.StartSpan("onCampaignContainers-POST", pCtx, nil) + campaign := model.CampaignContainerState{ + ObjectMeta: model.ObjectMeta{ + Name: id, + Namespace: namespace, + }, + Spec: &model.CampaignContainerSpec{}, + } + + err := c.CampaignContainersManager.UpsertState(ctx, id, campaign) + if err != nil { + ccLog.Infof("V (CampaignContainers): onCampaignContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + case fasthttp.MethodDelete: + ctx, span := observability.StartSpan("onCampaignContainers-DELETE", pCtx, nil) + err := c.CampaignContainersManager.DeleteState(ctx, id, namespace) + if err != nil { + ccLog.Infof("V (CampaignContainers): onCampaignContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + } + ccLog.Infof("V (CampaignContainers): onCampaignContainers failed - 405 method not allowed, traceId: %s", span.SpanContext().TraceID().String()) + resp := v1alpha2.COAResponse{ + State: v1alpha2.MethodNotAllowed, + Body: []byte("{\"result\":\"405 - method not allowed\"}"), + ContentType: "application/json", + } + observ_utils.UpdateSpanStatusFromCOAResponse(span, resp) + return resp +} diff --git a/api/pkg/apis/v1alpha1/vendors/campaigns-container-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/campaigns-container-vendor_test.go new file mode 100644 index 000000000..16c405d54 --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/campaigns-container-vendor_test.go @@ -0,0 +1,142 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "context" + "encoding/json" + "testing" + + sym_mgr "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub/memory" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" +) + +func TestCampaignContainersEndpoints(t *testing.T) { + vendor := createCampaignContainersVendor() + vendor.Route = "campaigncontainers" + endpoints := vendor.GetEndpoints() + assert.Equal(t, 1, len(endpoints)) +} + +func TestCampaignContainersInfo(t *testing.T) { + vendor := createCampaignContainersVendor() + vendor.Version = "1.0" + info := vendor.GetInfo() + assert.NotNil(t, info) + assert.Equal(t, "1.0", info.Version) +} + +func createCampaignContainersVendor() CampaignContainersVendor { + stateProvider := memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + vendor := CampaignContainersVendor{} + vendor.Init(vendors.VendorConfig{ + Properties: map[string]string{ + "test": "true", + }, + Managers: []managers.ManagerConfig{ + { + Name: "campaign-container-manager", + Type: "managers.symphony.campaigncontainers", + Properties: map[string]string{ + "providers.state": "mem-state", + }, + Providers: map[string]managers.ProviderConfig{ + "mem-state": { + Type: "providers.state.memory", + Config: memorystate.MemoryStateProviderConfig{}, + }, + }, + }, + }, + }, []managers.IManagerFactroy{ + &sym_mgr.SymphonyManagerFactory{}, + }, map[string]map[string]providers.IProvider{ + "campaign-container-manager": { + "mem-state": &stateProvider, + }, + }, nil) + return vendor +} + +func TestOnCampaignContainers(t *testing.T) { + vendor := createCampaignContainersVendor() + vendor.Context = &contexts.VendorContext{} + vendor.Context.SiteInfo = v1alpha2.SiteInfo{ + SiteId: "fake", + } + pubSubProvider := memory.InMemoryPubSubProvider{} + pubSubProvider.Init(memory.InMemoryPubSubConfig{Name: "test"}) + vendor.Context.Init(&pubSubProvider) + campaign := model.CampaignContainerState{ + Spec: &model.CampaignContainerSpec{}, + ObjectMeta: model.ObjectMeta{ + Name: "campaign1", + Namespace: "scope1", + }, + } + data, _ := json.Marshal(campaign) + resp := vendor.onCampaignContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodPost, + Body: data, + Parameters: map[string]string{ + "__name": "campaign1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + + resp = vendor.onCampaignContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "__name": "campaign1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + var campaigns model.CampaignContainerState + assert.Equal(t, v1alpha2.OK, resp.State) + err := json.Unmarshal(resp.Body, &campaigns) + assert.Nil(t, err) + assert.Equal(t, "campaign1", campaigns.ObjectMeta.Name) + assert.Equal(t, "scope1", campaigns.ObjectMeta.Namespace) + + resp = vendor.onCampaignContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + var campaignsList []model.CampaignContainerState + err = json.Unmarshal(resp.Body, &campaignsList) + assert.Nil(t, err) + assert.Equal(t, 1, len(campaignsList)) + assert.Equal(t, "campaign1", campaignsList[0].ObjectMeta.Name) + assert.Equal(t, "scope1", campaignsList[0].ObjectMeta.Namespace) + + resp = vendor.onCampaignContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodDelete, + Parameters: map[string]string{ + "__name": "campaign1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) +} diff --git a/api/pkg/apis/v1alpha1/vendors/catalogs-container-vendor.go b/api/pkg/apis/v1alpha1/vendors/catalogs-container-vendor.go new file mode 100644 index 000000000..25b782576 --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/catalogs-container-vendor.go @@ -0,0 +1,163 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/catalogcontainers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + "github.com/valyala/fasthttp" +) + +var ctLog = logger.NewLogger("coa.runtime") + +type CatalogContainersVendor struct { + vendors.Vendor + CatalogContainersManager *catalogcontainers.CatalogContainersManager +} + +func (o *CatalogContainersVendor) GetInfo() vendors.VendorInfo { + return vendors.VendorInfo{ + Version: o.Vendor.Version, + Name: "CatalogContainers", + Producer: "Microsoft", + } +} + +func (e *CatalogContainersVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error { + err := e.Vendor.Init(config, factories, providers, pubsubProvider) + if err != nil { + return err + } + for _, m := range e.Managers { + if c, ok := m.(*catalogcontainers.CatalogContainersManager); ok { + e.CatalogContainersManager = c + } + } + if e.CatalogContainersManager == nil { + return v1alpha2.NewCOAError(nil, "Catalog container manager is not supplied", v1alpha2.MissingConfig) + } + return nil +} + +func (o *CatalogContainersVendor) GetEndpoints() []v1alpha2.Endpoint { + route := "catalogcontainers" + if o.Route != "" { + route = o.Route + } + return []v1alpha2.Endpoint{ + { + Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete}, + Route: route, + Version: o.Version, + Handler: o.onCatalogContainers, + Parameters: []string{"name?"}, + }, + } +} + +func (c *CatalogContainersVendor) onCatalogContainers(request v1alpha2.COARequest) v1alpha2.COAResponse { + pCtx, span := observability.StartSpan("onCatalogContainers", request.Context, &map[string]string{ + "method": "onCatalogContainers", + }) + defer span.End() + ctLog.Infof("V (CatalogContainers): onCatalogContainers, method: %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) + + id := request.Parameters["__name"] + namespace, exist := request.Parameters["namespace"] + if !exist { + namespace = constants.DefaultScope + } + + ctLog.Infof("V (CatalogContainers): onCatalogContainers, method: %s, traceId: %s", string(request.Method), span.SpanContext().TraceID().String()) + switch request.Method { + case fasthttp.MethodGet: + ctx, span := observability.StartSpan("onCatalogContainers-GET", pCtx, nil) + var err error + var state interface{} + isArray := false + if id == "" { + // Change partition back to empty to indicate ListSpec need to query all namespaces + if !exist { + namespace = "" + } + state, err = c.CatalogContainersManager.ListState(ctx, namespace) + isArray = true + } else { + state, err = c.CatalogContainersManager.GetState(ctx, id, namespace) + } + if err != nil { + ctLog.Errorf("V (CatalogContainers): onCatalogContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + jData, _ := utils.FormatObject(state, isArray, request.Parameters["path"], request.Parameters["doc-type"]) + resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + Body: jData, + ContentType: "application/json", + }) + if request.Parameters["doc-type"] == "yaml" { + resp.ContentType = "application/text" + } + return resp + case fasthttp.MethodPost: + ctx, span := observability.StartSpan("onCatalogContainers-POST", pCtx, nil) + catalog := model.CatalogContainerState{ + ObjectMeta: model.ObjectMeta{ + Name: id, + Namespace: namespace, + }, + Spec: &model.CatalogContainerSpec{}, + } + + err := c.CatalogContainersManager.UpsertState(ctx, id, catalog) + if err != nil { + ctLog.Infof("V (CatalogContainers): onCatalogContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + case fasthttp.MethodDelete: + ctx, span := observability.StartSpan("onCatalogContainers-DELETE", pCtx, nil) + err := c.CatalogContainersManager.DeleteState(ctx, id, namespace) + if err != nil { + ctLog.Infof("V (CatalogContainers): onCatalogContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + } + ctLog.Infof("V (CatalogContainers): onCatalogContainers failed - 405 method not allowed, traceId: %s", span.SpanContext().TraceID().String()) + resp := v1alpha2.COAResponse{ + State: v1alpha2.MethodNotAllowed, + Body: []byte("{\"result\":\"405 - method not allowed\"}"), + ContentType: "application/json", + } + observ_utils.UpdateSpanStatusFromCOAResponse(span, resp) + return resp +} diff --git a/api/pkg/apis/v1alpha1/vendors/catalogs-container-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/catalogs-container-vendor_test.go new file mode 100644 index 000000000..3c5f68719 --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/catalogs-container-vendor_test.go @@ -0,0 +1,142 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "context" + "encoding/json" + "testing" + + sym_mgr "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub/memory" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" +) + +func TestCatalogContainersEndpoints(t *testing.T) { + vendor := createCatalogContainersVendor() + vendor.Route = "catalogcontainers" + endpoints := vendor.GetEndpoints() + assert.Equal(t, 1, len(endpoints)) +} + +func TestCatalogContainersInfo(t *testing.T) { + vendor := createCatalogContainersVendor() + vendor.Version = "1.0" + info := vendor.GetInfo() + assert.NotNil(t, info) + assert.Equal(t, "1.0", info.Version) +} + +func createCatalogContainersVendor() CatalogContainersVendor { + stateProvider := memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + vendor := CatalogContainersVendor{} + vendor.Init(vendors.VendorConfig{ + Properties: map[string]string{ + "test": "true", + }, + Managers: []managers.ManagerConfig{ + { + Name: "catalog-container-manager", + Type: "managers.symphony.catalogcontainers", + Properties: map[string]string{ + "providers.state": "mem-state", + }, + Providers: map[string]managers.ProviderConfig{ + "mem-state": { + Type: "providers.state.memory", + Config: memorystate.MemoryStateProviderConfig{}, + }, + }, + }, + }, + }, []managers.IManagerFactroy{ + &sym_mgr.SymphonyManagerFactory{}, + }, map[string]map[string]providers.IProvider{ + "catalog-container-manager": { + "mem-state": &stateProvider, + }, + }, nil) + return vendor +} + +func TestOnCatalogContainers(t *testing.T) { + vendor := createCatalogContainersVendor() + vendor.Context = &contexts.VendorContext{} + vendor.Context.SiteInfo = v1alpha2.SiteInfo{ + SiteId: "fake", + } + pubSubProvider := memory.InMemoryPubSubProvider{} + pubSubProvider.Init(memory.InMemoryPubSubConfig{Name: "test"}) + vendor.Context.Init(&pubSubProvider) + catalog := model.CatalogContainerState{ + Spec: &model.CatalogContainerSpec{}, + ObjectMeta: model.ObjectMeta{ + Name: "catalog1", + Namespace: "scope1", + }, + } + data, _ := json.Marshal(catalog) + resp := vendor.onCatalogContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodPost, + Body: data, + Parameters: map[string]string{ + "__name": "catalog1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + + resp = vendor.onCatalogContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "__name": "catalog1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + var catalogs model.CatalogContainerState + assert.Equal(t, v1alpha2.OK, resp.State) + err := json.Unmarshal(resp.Body, &catalogs) + assert.Nil(t, err) + assert.Equal(t, "catalog1", catalogs.ObjectMeta.Name) + assert.Equal(t, "scope1", catalogs.ObjectMeta.Namespace) + + resp = vendor.onCatalogContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + var catalogsList []model.CatalogContainerState + err = json.Unmarshal(resp.Body, &catalogsList) + assert.Nil(t, err) + assert.Equal(t, 1, len(catalogsList)) + assert.Equal(t, "catalog1", catalogsList[0].ObjectMeta.Name) + assert.Equal(t, "scope1", catalogsList[0].ObjectMeta.Namespace) + + resp = vendor.onCatalogContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodDelete, + Parameters: map[string]string{ + "__name": "catalog1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) +} diff --git a/api/pkg/apis/v1alpha1/vendors/instances-container-vendor.go b/api/pkg/apis/v1alpha1/vendors/instances-container-vendor.go new file mode 100644 index 000000000..2b76bf412 --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/instances-container-vendor.go @@ -0,0 +1,163 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/instancecontainers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + "github.com/valyala/fasthttp" +) + +var icLog = logger.NewLogger("coa.runtime") + +type InstanceContainersVendor struct { + vendors.Vendor + InstanceContainersManager *instancecontainers.InstanceContainersManager +} + +func (o *InstanceContainersVendor) GetInfo() vendors.VendorInfo { + return vendors.VendorInfo{ + Version: o.Vendor.Version, + Name: "InstanceContainers", + Producer: "Microsoft", + } +} + +func (e *InstanceContainersVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error { + err := e.Vendor.Init(config, factories, providers, pubsubProvider) + if err != nil { + return err + } + for _, m := range e.Managers { + if c, ok := m.(*instancecontainers.InstanceContainersManager); ok { + e.InstanceContainersManager = c + } + } + if e.InstanceContainersManager == nil { + return v1alpha2.NewCOAError(nil, "instance container manager is not supplied", v1alpha2.MissingConfig) + } + return nil +} + +func (o *InstanceContainersVendor) GetEndpoints() []v1alpha2.Endpoint { + route := "instancecontainers" + if o.Route != "" { + route = o.Route + } + return []v1alpha2.Endpoint{ + { + Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete}, + Route: route, + Version: o.Version, + Handler: o.onInstanceContainers, + Parameters: []string{"name?"}, + }, + } +} + +func (c *InstanceContainersVendor) onInstanceContainers(request v1alpha2.COARequest) v1alpha2.COAResponse { + pCtx, span := observability.StartSpan("onInstanceContainers", request.Context, &map[string]string{ + "method": "onInstanceContainers", + }) + defer span.End() + icLog.Infof("V (InstanceContainers): onInstanceContainers, method: %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) + + id := request.Parameters["__name"] + namespace, exist := request.Parameters["namespace"] + if !exist { + namespace = constants.DefaultScope + } + + icLog.Infof("V (InstanceContainers): onInstanceContainers, method: %s, traceId: %s", string(request.Method), span.SpanContext().TraceID().String()) + switch request.Method { + case fasthttp.MethodGet: + ctx, span := observability.StartSpan("onInstanceContainers-GET", pCtx, nil) + var err error + var state interface{} + isArray := false + if id == "" { + // Change partition back to empty to indicate ListSpec need to query all namespaces + if !exist { + namespace = "" + } + state, err = c.InstanceContainersManager.ListState(ctx, namespace) + isArray = true + } else { + state, err = c.InstanceContainersManager.GetState(ctx, id, namespace) + } + if err != nil { + icLog.Errorf("V (InstanceContainers): onInstanceContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + jData, _ := utils.FormatObject(state, isArray, request.Parameters["path"], request.Parameters["doc-type"]) + resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + Body: jData, + ContentType: "application/json", + }) + if request.Parameters["doc-type"] == "yaml" { + resp.ContentType = "application/text" + } + return resp + case fasthttp.MethodPost: + ctx, span := observability.StartSpan("onInstanceContainers-POST", pCtx, nil) + instance := model.InstanceContainerState{ + ObjectMeta: model.ObjectMeta{ + Name: id, + Namespace: namespace, + }, + Spec: &model.InstanceContainerSpec{}, + } + + err := c.InstanceContainersManager.UpsertState(ctx, id, instance) + if err != nil { + icLog.Infof("V (InstanceContainers): onInstanceContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + case fasthttp.MethodDelete: + ctx, span := observability.StartSpan("onInstanceContainers-DELETE", pCtx, nil) + err := c.InstanceContainersManager.DeleteState(ctx, id, namespace) + if err != nil { + icLog.Infof("V (InstanceContainers): onInstanceContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + } + icLog.Infof("V (InstanceContainers): onInstanceContainers failed - 405 method not allowed, traceId: %s", span.SpanContext().TraceID().String()) + resp := v1alpha2.COAResponse{ + State: v1alpha2.MethodNotAllowed, + Body: []byte("{\"result\":\"405 - method not allowed\"}"), + ContentType: "application/json", + } + observ_utils.UpdateSpanStatusFromCOAResponse(span, resp) + return resp +} diff --git a/api/pkg/apis/v1alpha1/vendors/instances-container-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/instances-container-vendor_test.go new file mode 100644 index 000000000..4c10a9eb3 --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/instances-container-vendor_test.go @@ -0,0 +1,142 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "context" + "encoding/json" + "testing" + + sym_mgr "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub/memory" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" +) + +func TestInstanceContainersEndpoints(t *testing.T) { + vendor := createInstanceContainersVendor() + vendor.Route = "instancecontainers" + endpoints := vendor.GetEndpoints() + assert.Equal(t, 1, len(endpoints)) +} + +func TestInstanceContainersInfo(t *testing.T) { + vendor := createInstanceContainersVendor() + vendor.Version = "1.0" + info := vendor.GetInfo() + assert.NotNil(t, info) + assert.Equal(t, "1.0", info.Version) +} + +func createInstanceContainersVendor() InstanceContainersVendor { + stateProvider := memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + vendor := InstanceContainersVendor{} + vendor.Init(vendors.VendorConfig{ + Properties: map[string]string{ + "test": "true", + }, + Managers: []managers.ManagerConfig{ + { + Name: "instance-container-manager", + Type: "managers.symphony.instancecontainers", + Properties: map[string]string{ + "providers.state": "mem-state", + }, + Providers: map[string]managers.ProviderConfig{ + "mem-state": { + Type: "providers.state.memory", + Config: memorystate.MemoryStateProviderConfig{}, + }, + }, + }, + }, + }, []managers.IManagerFactroy{ + &sym_mgr.SymphonyManagerFactory{}, + }, map[string]map[string]providers.IProvider{ + "instance-container-manager": { + "mem-state": &stateProvider, + }, + }, nil) + return vendor +} + +func TestOnInstanceContainers(t *testing.T) { + vendor := createInstanceContainersVendor() + vendor.Context = &contexts.VendorContext{} + vendor.Context.SiteInfo = v1alpha2.SiteInfo{ + SiteId: "fake", + } + pubSubProvider := memory.InMemoryPubSubProvider{} + pubSubProvider.Init(memory.InMemoryPubSubConfig{Name: "test"}) + vendor.Context.Init(&pubSubProvider) + instance := model.InstanceContainerState{ + Spec: &model.InstanceContainerSpec{}, + ObjectMeta: model.ObjectMeta{ + Name: "instance1", + Namespace: "scope1", + }, + } + data, _ := json.Marshal(instance) + resp := vendor.onInstanceContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodPost, + Body: data, + Parameters: map[string]string{ + "__name": "instance1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + + resp = vendor.onInstanceContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "__name": "instance1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + var instances model.InstanceContainerState + assert.Equal(t, v1alpha2.OK, resp.State) + err := json.Unmarshal(resp.Body, &instances) + assert.Nil(t, err) + assert.Equal(t, "instance1", instances.ObjectMeta.Name) + assert.Equal(t, "scope1", instances.ObjectMeta.Namespace) + + resp = vendor.onInstanceContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + var instancesList []model.InstanceContainerState + err = json.Unmarshal(resp.Body, &instancesList) + assert.Nil(t, err) + assert.Equal(t, 1, len(instancesList)) + assert.Equal(t, "instance1", instancesList[0].ObjectMeta.Name) + assert.Equal(t, "scope1", instancesList[0].ObjectMeta.Namespace) + + resp = vendor.onInstanceContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodDelete, + Parameters: map[string]string{ + "__name": "instance1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) +} diff --git a/api/pkg/apis/v1alpha1/vendors/solutions-container-vendor.go b/api/pkg/apis/v1alpha1/vendors/solutions-container-vendor.go new file mode 100644 index 000000000..360109c2d --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/solutions-container-vendor.go @@ -0,0 +1,163 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solutioncontainers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + "github.com/valyala/fasthttp" +) + +var scLog = logger.NewLogger("coa.runtime") + +type SolutionContainersVendor struct { + vendors.Vendor + SolutionContainersManager *solutioncontainers.SolutionContainersManager +} + +func (o *SolutionContainersVendor) GetInfo() vendors.VendorInfo { + return vendors.VendorInfo{ + Version: o.Vendor.Version, + Name: "SolutionContainers", + Producer: "Microsoft", + } +} + +func (e *SolutionContainersVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error { + err := e.Vendor.Init(config, factories, providers, pubsubProvider) + if err != nil { + return err + } + for _, m := range e.Managers { + if c, ok := m.(*solutioncontainers.SolutionContainersManager); ok { + e.SolutionContainersManager = c + } + } + if e.SolutionContainersManager == nil { + return v1alpha2.NewCOAError(nil, "solution container manager is not supplied", v1alpha2.MissingConfig) + } + return nil +} + +func (o *SolutionContainersVendor) GetEndpoints() []v1alpha2.Endpoint { + route := "solutioncontainers" + if o.Route != "" { + route = o.Route + } + return []v1alpha2.Endpoint{ + { + Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete}, + Route: route, + Version: o.Version, + Handler: o.onSolutionContainers, + Parameters: []string{"name?"}, + }, + } +} + +func (c *SolutionContainersVendor) onSolutionContainers(request v1alpha2.COARequest) v1alpha2.COAResponse { + pCtx, span := observability.StartSpan("onSolutionContainers", request.Context, &map[string]string{ + "method": "onSolutionContainers", + }) + defer span.End() + scLog.Infof("V (SolutionContainers): onSolutionContainers, method: %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) + + id := request.Parameters["__name"] + namespace, exist := request.Parameters["namespace"] + if !exist { + namespace = constants.DefaultScope + } + + scLog.Infof("V (SolutionContainers): onSolutionContainers, method: %s, traceId: %s", string(request.Method), span.SpanContext().TraceID().String()) + switch request.Method { + case fasthttp.MethodGet: + ctx, span := observability.StartSpan("onSolutionContainers-GET", pCtx, nil) + var err error + var state interface{} + isArray := false + if id == "" { + // Change partition back to empty to indicate ListSpec need to query all namespaces + if !exist { + namespace = "" + } + state, err = c.SolutionContainersManager.ListState(ctx, namespace) + isArray = true + } else { + state, err = c.SolutionContainersManager.GetState(ctx, id, namespace) + } + if err != nil { + scLog.Errorf("V (SolutionContainers): onSolutionContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + jData, _ := utils.FormatObject(state, isArray, request.Parameters["path"], request.Parameters["doc-type"]) + resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + Body: jData, + ContentType: "application/json", + }) + if request.Parameters["doc-type"] == "yaml" { + resp.ContentType = "application/text" + } + return resp + case fasthttp.MethodPost: + ctx, span := observability.StartSpan("onSolutionContainers-POST", pCtx, nil) + solution := model.SolutionContainerState{ + ObjectMeta: model.ObjectMeta{ + Name: id, + Namespace: namespace, + }, + Spec: &model.SolutionContainerSpec{}, + } + + err := c.SolutionContainersManager.UpsertState(ctx, id, solution) + if err != nil { + scLog.Infof("V (SolutionContainers): onSolutionContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + case fasthttp.MethodDelete: + ctx, span := observability.StartSpan("onSolutionContainers-DELETE", pCtx, nil) + err := c.SolutionContainersManager.DeleteState(ctx, id, namespace) + if err != nil { + scLog.Infof("V (SolutionContainers): onSolutionContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + } + scLog.Infof("V (SolutionContainers): onSolutionContainers failed - 405 method not allowed, traceId: %s", span.SpanContext().TraceID().String()) + resp := v1alpha2.COAResponse{ + State: v1alpha2.MethodNotAllowed, + Body: []byte("{\"result\":\"405 - method not allowed\"}"), + ContentType: "application/json", + } + observ_utils.UpdateSpanStatusFromCOAResponse(span, resp) + return resp +} diff --git a/api/pkg/apis/v1alpha1/vendors/solutions-container-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/solutions-container-vendor_test.go new file mode 100644 index 000000000..6561468fa --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/solutions-container-vendor_test.go @@ -0,0 +1,142 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "context" + "encoding/json" + "testing" + + sym_mgr "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub/memory" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" +) + +func TestSolutionContainersEndpoints(t *testing.T) { + vendor := createSolutionContainersVendor() + vendor.Route = "solutioncontainers" + endpoints := vendor.GetEndpoints() + assert.Equal(t, 1, len(endpoints)) +} + +func TestSolutionContainersInfo(t *testing.T) { + vendor := createSolutionContainersVendor() + vendor.Version = "1.0" + info := vendor.GetInfo() + assert.NotNil(t, info) + assert.Equal(t, "1.0", info.Version) +} + +func createSolutionContainersVendor() SolutionContainersVendor { + stateProvider := memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + vendor := SolutionContainersVendor{} + vendor.Init(vendors.VendorConfig{ + Properties: map[string]string{ + "test": "true", + }, + Managers: []managers.ManagerConfig{ + { + Name: "solution-container-manager", + Type: "managers.symphony.solutioncontainers", + Properties: map[string]string{ + "providers.state": "mem-state", + }, + Providers: map[string]managers.ProviderConfig{ + "mem-state": { + Type: "providers.state.memory", + Config: memorystate.MemoryStateProviderConfig{}, + }, + }, + }, + }, + }, []managers.IManagerFactroy{ + &sym_mgr.SymphonyManagerFactory{}, + }, map[string]map[string]providers.IProvider{ + "solution-container-manager": { + "mem-state": &stateProvider, + }, + }, nil) + return vendor +} + +func TestOnSolutionContainers(t *testing.T) { + vendor := createSolutionContainersVendor() + vendor.Context = &contexts.VendorContext{} + vendor.Context.SiteInfo = v1alpha2.SiteInfo{ + SiteId: "fake", + } + pubSubProvider := memory.InMemoryPubSubProvider{} + pubSubProvider.Init(memory.InMemoryPubSubConfig{Name: "test"}) + vendor.Context.Init(&pubSubProvider) + solution := model.SolutionContainerState{ + Spec: &model.SolutionContainerSpec{}, + ObjectMeta: model.ObjectMeta{ + Name: "solution1", + Namespace: "scope1", + }, + } + data, _ := json.Marshal(solution) + resp := vendor.onSolutionContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodPost, + Body: data, + Parameters: map[string]string{ + "__name": "solution1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + + resp = vendor.onSolutionContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "__name": "solution1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + var solutions model.SolutionContainerState + assert.Equal(t, v1alpha2.OK, resp.State) + err := json.Unmarshal(resp.Body, &solutions) + assert.Nil(t, err) + assert.Equal(t, "solution1", solutions.ObjectMeta.Name) + assert.Equal(t, "scope1", solutions.ObjectMeta.Namespace) + + resp = vendor.onSolutionContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + var solutionsList []model.SolutionContainerState + err = json.Unmarshal(resp.Body, &solutionsList) + assert.Nil(t, err) + assert.Equal(t, 1, len(solutionsList)) + assert.Equal(t, "solution1", solutionsList[0].ObjectMeta.Name) + assert.Equal(t, "scope1", solutionsList[0].ObjectMeta.Namespace) + + resp = vendor.onSolutionContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodDelete, + Parameters: map[string]string{ + "__name": "solution1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) +} diff --git a/api/pkg/apis/v1alpha1/vendors/targets-container-vendor.go b/api/pkg/apis/v1alpha1/vendors/targets-container-vendor.go new file mode 100644 index 000000000..d5f870bbe --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/targets-container-vendor.go @@ -0,0 +1,163 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "github.com/eclipse-symphony/symphony/api/constants" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/targetcontainers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability" + observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/eclipse-symphony/symphony/coa/pkg/logger" + "github.com/valyala/fasthttp" +) + +var tcLog = logger.NewLogger("coa.runtime") + +type TargetContainersVendor struct { + vendors.Vendor + TargetContainersManager *targetcontainers.TargetContainersManager +} + +func (o *TargetContainersVendor) GetInfo() vendors.VendorInfo { + return vendors.VendorInfo{ + Version: o.Vendor.Version, + Name: "TargetContainers", + Producer: "Microsoft", + } +} + +func (e *TargetContainersVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error { + err := e.Vendor.Init(config, factories, providers, pubsubProvider) + if err != nil { + return err + } + for _, m := range e.Managers { + if c, ok := m.(*targetcontainers.TargetContainersManager); ok { + e.TargetContainersManager = c + } + } + if e.TargetContainersManager == nil { + return v1alpha2.NewCOAError(nil, "target container manager is not supplied", v1alpha2.MissingConfig) + } + return nil +} + +func (o *TargetContainersVendor) GetEndpoints() []v1alpha2.Endpoint { + route := "targetcontainers" + if o.Route != "" { + route = o.Route + } + return []v1alpha2.Endpoint{ + { + Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete}, + Route: route, + Version: o.Version, + Handler: o.onTargetContainers, + Parameters: []string{"name?"}, + }, + } +} + +func (c *TargetContainersVendor) onTargetContainers(request v1alpha2.COARequest) v1alpha2.COAResponse { + pCtx, span := observability.StartSpan("onTargetContainers", request.Context, &map[string]string{ + "method": "onTargetContainers", + }) + defer span.End() + tcLog.Infof("V (TargetContainers): onTargetContainers, method: %s, traceId: %s", request.Method, span.SpanContext().TraceID().String()) + + id := request.Parameters["__name"] + namespace, exist := request.Parameters["namespace"] + if !exist { + namespace = constants.DefaultScope + } + + tcLog.Infof("V (TargetContainers): onTargetContainers, method: %s, traceId: %s", string(request.Method), span.SpanContext().TraceID().String()) + switch request.Method { + case fasthttp.MethodGet: + ctx, span := observability.StartSpan("onTargetContainers-GET", pCtx, nil) + var err error + var state interface{} + isArray := false + if id == "" { + // Change partition back to empty to indicate ListSpec need to query all namespaces + if !exist { + namespace = "" + } + state, err = c.TargetContainersManager.ListState(ctx, namespace) + isArray = true + } else { + state, err = c.TargetContainersManager.GetState(ctx, id, namespace) + } + if err != nil { + tcLog.Errorf("V (TargetContainers): onTargetContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + jData, _ := utils.FormatObject(state, isArray, request.Parameters["path"], request.Parameters["doc-type"]) + resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + Body: jData, + ContentType: "application/json", + }) + if request.Parameters["doc-type"] == "yaml" { + resp.ContentType = "application/text" + } + return resp + case fasthttp.MethodPost: + ctx, span := observability.StartSpan("onTargetContainers-POST", pCtx, nil) + target := model.TargetContainerState{ + ObjectMeta: model.ObjectMeta{ + Name: id, + Namespace: namespace, + }, + Spec: &model.TargetContainerSpec{}, + } + + err := c.TargetContainersManager.UpsertState(ctx, id, target) + if err != nil { + tcLog.Infof("V (TargetContainers): onTargetContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + case fasthttp.MethodDelete: + ctx, span := observability.StartSpan("onTargetContainers-DELETE", pCtx, nil) + err := c.TargetContainersManager.DeleteState(ctx, id, namespace) + if err != nil { + tcLog.Infof("V (TargetContainers): onTargetContainers failed - %s, traceId: %s", err.Error(), span.SpanContext().TraceID().String()) + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.InternalError, + Body: []byte(err.Error()), + }) + } + return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{ + State: v1alpha2.OK, + }) + } + tcLog.Infof("V (TargetContainers): onTargetContainers failed - 405 method not allowed, traceId: %s", span.SpanContext().TraceID().String()) + resp := v1alpha2.COAResponse{ + State: v1alpha2.MethodNotAllowed, + Body: []byte("{\"result\":\"405 - method not allowed\"}"), + ContentType: "application/json", + } + observ_utils.UpdateSpanStatusFromCOAResponse(span, resp) + return resp +} diff --git a/api/pkg/apis/v1alpha1/vendors/targets-container-vendor_test.go b/api/pkg/apis/v1alpha1/vendors/targets-container-vendor_test.go new file mode 100644 index 000000000..cb327ccda --- /dev/null +++ b/api/pkg/apis/v1alpha1/vendors/targets-container-vendor_test.go @@ -0,0 +1,142 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package vendors + +import ( + "context" + "encoding/json" + "testing" + + sym_mgr "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub/memory" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states/memorystate" + "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors" + "github.com/stretchr/testify/assert" + "github.com/valyala/fasthttp" +) + +func TestTargetContainersEndpoints(t *testing.T) { + vendor := createTargetContainersVendor() + vendor.Route = "targetcontainers" + endpoints := vendor.GetEndpoints() + assert.Equal(t, 1, len(endpoints)) +} + +func TestTargetContainersInfo(t *testing.T) { + vendor := createTargetContainersVendor() + vendor.Version = "1.0" + info := vendor.GetInfo() + assert.NotNil(t, info) + assert.Equal(t, "1.0", info.Version) +} + +func createTargetContainersVendor() TargetContainersVendor { + stateProvider := memorystate.MemoryStateProvider{} + stateProvider.Init(memorystate.MemoryStateProviderConfig{}) + vendor := TargetContainersVendor{} + vendor.Init(vendors.VendorConfig{ + Properties: map[string]string{ + "test": "true", + }, + Managers: []managers.ManagerConfig{ + { + Name: "target-container-manager", + Type: "managers.symphony.targetcontainers", + Properties: map[string]string{ + "providers.state": "mem-state", + }, + Providers: map[string]managers.ProviderConfig{ + "mem-state": { + Type: "providers.state.memory", + Config: memorystate.MemoryStateProviderConfig{}, + }, + }, + }, + }, + }, []managers.IManagerFactroy{ + &sym_mgr.SymphonyManagerFactory{}, + }, map[string]map[string]providers.IProvider{ + "target-container-manager": { + "mem-state": &stateProvider, + }, + }, nil) + return vendor +} + +func TestOnTargetContainers(t *testing.T) { + vendor := createTargetContainersVendor() + vendor.Context = &contexts.VendorContext{} + vendor.Context.SiteInfo = v1alpha2.SiteInfo{ + SiteId: "fake", + } + pubSubProvider := memory.InMemoryPubSubProvider{} + pubSubProvider.Init(memory.InMemoryPubSubConfig{Name: "test"}) + vendor.Context.Init(&pubSubProvider) + target := model.TargetContainerState{ + Spec: &model.TargetContainerSpec{}, + ObjectMeta: model.ObjectMeta{ + Name: "target1", + Namespace: "scope1", + }, + } + data, _ := json.Marshal(target) + resp := vendor.onTargetContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodPost, + Body: data, + Parameters: map[string]string{ + "__name": "target1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + + resp = vendor.onTargetContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "__name": "target1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + var targets model.TargetContainerState + assert.Equal(t, v1alpha2.OK, resp.State) + err := json.Unmarshal(resp.Body, &targets) + assert.Nil(t, err) + assert.Equal(t, "target1", targets.ObjectMeta.Name) + assert.Equal(t, "scope1", targets.ObjectMeta.Namespace) + + resp = vendor.onTargetContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodGet, + Parameters: map[string]string{ + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) + var targetsList []model.TargetContainerState + err = json.Unmarshal(resp.Body, &targetsList) + assert.Nil(t, err) + assert.Equal(t, 1, len(targetsList)) + assert.Equal(t, "target1", targetsList[0].ObjectMeta.Name) + assert.Equal(t, "scope1", targetsList[0].ObjectMeta.Namespace) + + resp = vendor.onTargetContainers(v1alpha2.COARequest{ + Method: fasthttp.MethodDelete, + Parameters: map[string]string{ + "__name": "target1", + "namespace": "scope1", + }, + Context: context.Background(), + }) + assert.Equal(t, v1alpha2.OK, resp.State) +} diff --git a/api/pkg/apis/v1alpha1/vendors/vendorfactory.go b/api/pkg/apis/v1alpha1/vendors/vendorfactory.go index 250490797..dc1c40358 100644 --- a/api/pkg/apis/v1alpha1/vendors/vendorfactory.go +++ b/api/pkg/apis/v1alpha1/vendors/vendorfactory.go @@ -23,16 +23,26 @@ func (c SymphonyVendorFactory) CreateVendor(config vendors.VendorConfig) (vendor return &AgentVendor{}, nil case "vendors.targets": return &TargetsVendor{}, nil + case "vendors.targetcontainers": + return &TargetContainersVendor{}, nil case "vendors.instances": return &InstancesVendor{}, nil + case "vendors.instancecontainers": + return &InstanceContainersVendor{}, nil case "vendors.devices": return &DevicesVendor{}, nil case "vendors.solutions": return &SolutionsVendor{}, nil + case "vendors.solutioncontainers": + return &SolutionContainersVendor{}, nil case "vendors.campaigns": return &CampaignsVendor{}, nil + case "vendors.campaigncontainers": + return &CampaignContainersVendor{}, nil case "vendors.catalogs": return &CatalogsVendor{}, nil + case "vendors.catalogcontainers": + return &CatalogContainersVendor{}, nil case "vendors.activations": return &ActivationsVendor{}, nil case "vendors.users": diff --git a/api/symphony-api-dev-console-trace.json b/api/symphony-api-dev-console-trace.json index abf9158ef..620e25f2c 100644 --- a/api/symphony-api-dev-console-trace.json +++ b/api/symphony-api-dev-console-trace.json @@ -81,6 +81,72 @@ } ] }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, { "type": "vendors.devices", "route": "devices", diff --git a/api/symphony-api-dev-zipkin-trace.json b/api/symphony-api-dev-zipkin-trace.json index 9823d9966..2e12fce8c 100644 --- a/api/symphony-api-dev-zipkin-trace.json +++ b/api/symphony-api-dev-zipkin-trace.json @@ -80,6 +80,72 @@ } ] }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, { "type": "vendors.devices", "route": "devices", diff --git a/api/symphony-api-dev.json b/api/symphony-api-dev.json index c0b40b39f..be98402d4 100644 --- a/api/symphony-api-dev.json +++ b/api/symphony-api-dev.json @@ -80,6 +80,72 @@ } ] }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": false, + "configType": "path" + } + } + } + } + ] + }, { "type": "vendors.devices", "route": "devices", diff --git a/api/symphony-api-no-k8s-munchen.json b/api/symphony-api-no-k8s-munchen.json index 746b83cfd..6d0c03a26 100644 --- a/api/symphony-api-no-k8s-munchen.json +++ b/api/symphony-api-no-k8s-munchen.json @@ -152,6 +152,25 @@ } ] }, + { + "type": "vendors.campaigncontainers", + "route": "campaigncontainers", + "managers": [ + { + "name": "campaign-container-manager", + "type": "managers.symphony.campaigncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.echo", "route": "greetings", @@ -249,6 +268,63 @@ "useJobManager": "true" } }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.devices", "loopInterval": 15, @@ -495,6 +571,26 @@ } ] }, + { + "type": "vendors.catalogcontainers", + "route": "catalogcontainers", + "managers": [ + { + "name": "catalog-container-manager", + "type": "managers.symphony.catalogcontainers", + "properties": { + "providers.state": "memeory", + "singleton": "true" + }, + "providers": { + "memeory": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.visualization", "route": "visualization", diff --git a/api/symphony-api-no-k8s-new-york.json b/api/symphony-api-no-k8s-new-york.json index 2751cfbc3..1abb5c34d 100644 --- a/api/symphony-api-no-k8s-new-york.json +++ b/api/symphony-api-no-k8s-new-york.json @@ -150,6 +150,25 @@ } ] }, + { + "type": "vendors.campaigncontainers", + "route": "campaigncontainers", + "managers": [ + { + "name": "campaign-container-manager", + "type": "managers.symphony.campaigncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.echo", "route": "greetings", @@ -247,6 +266,63 @@ "useJobManager": "true" } }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.devices", "loopInterval": 15, @@ -482,6 +558,26 @@ } ] }, + { + "type": "vendors.catalogcontainers", + "route": "catalogcontainers", + "managers": [ + { + "name": "catalog-container-manager", + "type": "managers.symphony.catalogcontainers", + "properties": { + "providers.state": "memeory", + "singleton": "true" + }, + "providers": { + "memeory": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.visualization", "route": "visualization", diff --git a/api/symphony-api-no-k8s-tokyo.json b/api/symphony-api-no-k8s-tokyo.json index 9e39f397f..0de7bf339 100644 --- a/api/symphony-api-no-k8s-tokyo.json +++ b/api/symphony-api-no-k8s-tokyo.json @@ -150,6 +150,25 @@ } ] }, + { + "type": "vendors.campaigncontainers", + "route": "campaigncontainers", + "managers": [ + { + "name": "campaign-container-manager", + "type": "managers.symphony.campaigncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.echo", "route": "greetings", @@ -247,6 +266,63 @@ "useJobManager": "true" } }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.devices", "loopInterval": 15, @@ -483,6 +559,26 @@ } ] }, + { + "type": "vendors.catalogcontainers", + "route": "catalogcontainers", + "managers": [ + { + "name": "catalog-container-manager", + "type": "managers.symphony.catalogcontainers", + "properties": { + "providers.state": "memeory", + "singleton": "true" + }, + "providers": { + "memeory": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.visualization", "route": "visualization", diff --git a/api/symphony-api-no-k8s.json b/api/symphony-api-no-k8s.json index f5f17ed95..116a47010 100644 --- a/api/symphony-api-no-k8s.json +++ b/api/symphony-api-no-k8s.json @@ -164,6 +164,25 @@ } ] }, + { + "type": "vendors.campaigncontainers", + "route": "campaigncontainers", + "managers": [ + { + "name": "campaign-container-manager", + "type": "managers.symphony.campaigncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.echo", "route": "greetings", @@ -260,6 +279,63 @@ "useJobManager": "true" } }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.devices", "loopInterval": 15, @@ -502,6 +578,26 @@ } ] }, + { + "type": "vendors.catalogcontainers", + "route": "catalogcontainers", + "managers": [ + { + "name": "catalog-container-manager", + "type": "managers.symphony.catalogcontainers", + "properties": { + "providers.state": "memeory", + "singleton": "true" + }, + "providers": { + "memeory": { + "type": "providers.state.memory", + "config": {} + } + } + } + ] + }, { "type": "vendors.visualization", "route": "visualization", diff --git a/api/symphony-api-production.json b/api/symphony-api-production.json index 83e83e489..49cc8b91b 100644 --- a/api/symphony-api-production.json +++ b/api/symphony-api-production.json @@ -181,6 +181,27 @@ } ] }, + { + "type": "vendors.campaigncontainers", + "route": "campaigncontainers", + "managers": [ + { + "name": "campaign-container-manager", + "type": "managers.symphony.campaigncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.echo", "route": "greetings", @@ -275,6 +296,69 @@ } ] }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.devices", "route": "devices", @@ -536,6 +620,28 @@ } ] }, + { + "type": "vendors.catalogcontainers", + "route": "catalogcontainers", + "managers": [ + { + "name": "catalog-container-manager", + "type": "managers.symphony.catalogcontainers", + "properties": { + "providers.state": "k8s-state", + "singleton": "true" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.visualization", "route": "visualization", diff --git a/api/symphony-api.json b/api/symphony-api.json index 5c6563877..f9f9a4531 100644 --- a/api/symphony-api.json +++ b/api/symphony-api.json @@ -13,17 +13,23 @@ "lng": "-122.12826" }, "currentSite": { - "baseUrl": "http://symphony-service:8080/v1alpha2/", + "baseUrl": "https://symphony-service:8081/v1alpha2/", "username": "admin", "password": "" - } + } }, "api": { "pubsub": { "shared": true, "provider": { - "type": "providers.pubsub.memory", - "config": {} + "type": "providers.pubsub.redis", + "config": { + "name": "redis", + "host": "symphony-redis:6379", + "requireTLS": false, + "password": "", + "numberOfWorkers": 1 + } } }, "vendors": [ @@ -40,7 +46,6 @@ "catalog": { "type": "providers.config.catalog", "config": { - "baseUrl": "http://symphony-service:8080/v1alpha2/", "user": "admin", "password": "" } @@ -56,8 +61,10 @@ { "name": "stage-manager", "type": "managers.symphony.stage", - "properties": { - "providers.state": "k8s-state" + "properties": { + "user": "admin", + "password": "", + "providers.state": "k8s-state" }, "providers": { "k8s-state": { @@ -98,7 +105,13 @@ } } } - ] + ], + "properties": { + "wait.user": "admin", + "wait.password": "", + "wait.wait.interval": "15", + "wait.wait.count": "10" + } }, { "type": "vendors.activations", @@ -166,6 +179,27 @@ } ] }, + { + "type": "vendors.campaigncontainers", + "route": "campaigncontainers", + "managers": [ + { + "name": "campaign-container-manager", + "type": "managers.symphony.campaigncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.echo", "route": "greetings", @@ -181,12 +215,11 @@ "type": "managers.symphony.jobs", "properties": { "providers.state": "mem-state", - "baseUrl": "http://symphony-service:8080/v1alpha2/", "user": "admin", "password": "", "interval": "#15", "poll.enabled": "false", - "schedule.enabled": "true" + "schedule.enabled": "true" }, "providers": { "mem-state": { @@ -218,6 +251,27 @@ } ] }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.solutions", "route": "solutions", @@ -239,6 +293,27 @@ } ] }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.instances", "route": "instances", @@ -260,6 +335,27 @@ } ] }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.devices", "route": "devices", @@ -468,15 +564,15 @@ "properties": { "poll.enabled": "true", "interval": "#15", - "providers.queue": "memeory-queue", + "providers.queue": "memory-queue", "providers.state": "memory-state" }, "providers": { - "memeory-queue": { + "memory-queue": { "type": "providers.queue.memory", "config": {} }, - "mem-state": { + "memory-state": { "type": "providers.state.memory", "config": {} } @@ -487,10 +583,9 @@ "type": "managers.symphony.sync", "properties": { "interval": "#15", - "sync.enabled": "true", - "baseUrl": "http://symphony-service:8080/v1alpha2/", + "sync.enabled": "true" , "user": "admin", - "password": "" + "password": "" } } ] @@ -521,6 +616,28 @@ } ] }, + { + "type": "vendors.catalogcontainers", + "route": "catalogcontainers", + "managers": [ + { + "name": "catalog-container-manager", + "type": "managers.symphony.catalogcontainers", + "properties": { + "providers.state": "k8s-state", + "singleton": "true" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.visualization", "route": "visualization", @@ -554,7 +671,23 @@ "type": "bindings.http", "config": { "port": 8080, - "pipeline": [ + "pipeline": [ + { + "type": "middleware.http.tracing", + "properties": { + "pipeline": [ + { + "exporter": { + "type": "tracing.exporters.zipkin", + "backendUrl": "http://symphony-zipkin.default.svc.cluster.local:9411/api/v2/spans", + "sampler": { + "sampleRate": "always" + } + } + } + ] + } + }, { "type": "middleware.http.cors", "properties": { @@ -644,9 +777,64 @@ "port": 8081, "tls": true, "certProvider": { - "type": "certs.autogen", - "config":{} - } + "type": "certs.localfile", + "config": { + "name": "symphony-serving-cert", + "cert": "/etc/symphony-api/tls/tls.crt", + "key": "/etc/symphony-api/tls/tls.key" + } + }, + "pipeline": [ + { + "type": "middleware.http.tracing", + "properties": { + "pipeline": [ + { + "exporter": { + "type": "tracing.exporters.zipkin", + "backendUrl": "http://symphony-zipkin.default.svc.cluster.local:9411/api/v2/spans", + "sampler": { + "sampleRate": "always" + } + } + } + ] + } + }, + { + "type": "middleware.http.cors", + "properties": { + "Access-Control-Allow-Headers": "authorization,Content-Type", + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS", + "Access-Control-Allow-Origin": "*" + } + }, + { + "type": "middleware.http.jwt", + "properties": { + "ignorePaths": [ + "/v1alpha2/users/auth", + "/v1alpha2/solution/instances", + "/v1alpha2/agent/references", + "/v1alpha2/greetings", + "/v1alpha2/agent/config" + ], + "verifyKey": "SymphonyKey", + "authServer": "kubernetes", + "enableRBAC": false + } + }, + { + "type": "middleware.http.telemetry", + "properties": { + "enabled": true, + "maxBatchSize": 8192, + "maxBatchIntervalSeconds": 2, + "client": "my-dev-machine" + } + } + ] } } ] diff --git a/packages/helm/symphony/files/symphony-api.json b/packages/helm/symphony/files/symphony-api.json index bb6314b2f..f8e3ab4b9 100644 --- a/packages/helm/symphony/files/symphony-api.json +++ b/packages/helm/symphony/files/symphony-api.json @@ -194,6 +194,27 @@ } ] }, + { + "type": "vendors.campaigncontainers", + "route": "campaigncontainers", + "managers": [ + { + "name": "campaign-container-manager", + "type": "managers.symphony.campaigncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.echo", "route": "greetings", @@ -245,6 +266,27 @@ } ] }, + { + "type": "vendors.targetcontainers", + "route": "targetcontainers", + "managers": [ + { + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.solutions", "route": "solutions", @@ -266,6 +308,27 @@ } ] }, + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", + "managers": [ + { + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.instances", "route": "instances", @@ -287,6 +350,27 @@ } ] }, + { + "type": "vendors.instancecontainers", + "route": "instancecontainers", + "managers": [ + { + "name": "instance-container-manager", + "type": "managers.symphony.instancecontainers", + "properties": { + "providers.state": "k8s-state" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.devices", "route": "devices", @@ -547,6 +631,28 @@ } ] }, + { + "type": "vendors.catalogcontainers", + "route": "catalogcontainers", + "managers": [ + { + "name": "catalog-container-manager", + "type": "managers.symphony.catalogcontainers", + "properties": { + "providers.state": "k8s-state", + "singleton": "true" + }, + "providers": { + "k8s-state": { + "type": "providers.state.k8s", + "config": { + "inCluster": true + } + } + } + } + ] + }, { "type": "vendors.visualization", "route": "visualization", From 976068d416b6151b41d8603629f099ed2aaa55f3 Mon Sep 17 00:00:00 2001 From: Lency Qian Date: Thu, 6 Jun 2024 23:32:56 +0800 Subject: [PATCH 3/6] fix nondefault operation --- .../stage/materialize/materialize.go | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go index b9c4693da..0407ccb77 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go +++ b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go @@ -176,13 +176,13 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Instance name is invalid: catalog - %s", name), v1alpha2.BadRequest) } - mLog.Debugf(" P (Materialize Processor): check instance contains %v, namespace %s", instanceState.ObjectMeta.Name, instanceState.ObjectMeta.Namespace) - _, err := i.ApiClient.GetInstanceContainer(ctx, instanceState.Spec.RootResource, instanceState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + mLog.Debugf(" P (Materialize Processor): check instance contains %v, namespace %s", instanceState.ObjectMeta.Name, namespace) + _, err := i.ApiClient.GetInstanceContainer(ctx, instanceState.Spec.RootResource, namespace, i.Config.User, i.Config.Password) if err != nil && strings.Contains(err.Error(), constants.NotFound) { mLog.Debugf("Failed to get instance container %s: %s", instanceState.Spec.RootResource, err.Error()) - instanceContainerState := model.InstanceContainerState{ObjectMeta: model.ObjectMeta{Name: instanceState.Spec.RootResource, Namespace: instanceState.ObjectMeta.Namespace}} + instanceContainerState := model.InstanceContainerState{ObjectMeta: model.ObjectMeta{Name: instanceState.Spec.RootResource, Namespace: namespace}} containerObjectData, _ := json.Marshal(instanceContainerState) - err = i.ApiClient.CreateInstanceContainer(ctx, instanceState.Spec.RootResource, containerObjectData, instanceState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + err = i.ApiClient.CreateInstanceContainer(ctx, instanceState.Spec.RootResource, containerObjectData, namespace, i.Config.User, i.Config.Password) if err != nil { mLog.Errorf("Failed to create instance container %s: %s", instanceState.Spec.RootResource, err.Error()) return outputs, false, err @@ -221,14 +221,13 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid solution name: catalog - %s", name), v1alpha2.BadRequest) } - mLog.Debugf(" P (Materialize Processor): check solution contains %v, namespace %s", solutionState.Spec.RootResource, solutionState.ObjectMeta.Namespace) - _, err := i.ApiClient.GetSolutionContainer(ctx, solutionState.Spec.RootResource, solutionState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) - + mLog.Debugf(" P (Materialize Processor): check solution contains %v, namespace %s", solutionState.Spec.RootResource, namespace) + _, err := i.ApiClient.GetSolutionContainer(ctx, solutionState.Spec.RootResource, namespace, i.Config.User, i.Config.Password) if err != nil && strings.Contains(err.Error(), constants.NotFound) { mLog.Debugf("Failed to get solution container %s: %s", solutionState.Spec.RootResource, err.Error()) - solutionContainerState := model.SolutionContainerState{ObjectMeta: model.ObjectMeta{Name: solutionState.Spec.RootResource, Namespace: solutionState.ObjectMeta.Namespace}} + solutionContainerState := model.SolutionContainerState{ObjectMeta: model.ObjectMeta{Name: solutionState.Spec.RootResource, Namespace: namespace}} containerObjectData, _ := json.Marshal(solutionContainerState) - err = i.ApiClient.CreateSolutionContainer(ctx, solutionState.Spec.RootResource, containerObjectData, solutionState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + err = i.ApiClient.CreateSolutionContainer(ctx, solutionState.Spec.RootResource, containerObjectData, namespace, i.Config.User, i.Config.Password) if err != nil { mLog.Errorf("Failed to create solution container %s: %s", solutionState.Spec.RootResource, err.Error()) return outputs, false, err @@ -267,13 +266,13 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid target name: %s", name), v1alpha2.BadRequest) } - mLog.Debugf(" P (Materialize Processor): check target contains %v, namespace %s", targetState.Spec.RootResource, targetState.ObjectMeta.Namespace) - _, err := i.ApiClient.GetTargetContainer(ctx, targetState.Spec.RootResource, targetState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + mLog.Debugf(" P (Materialize Processor): check target contains %v, namespace %s", targetState.Spec.RootResource, namespace) + _, err := i.ApiClient.GetTargetContainer(ctx, targetState.Spec.RootResource, namespace, i.Config.User, i.Config.Password) if err != nil && strings.Contains(err.Error(), constants.NotFound) { mLog.Debugf("Failed to get target container %s: %s", targetState.Spec.RootResource, err.Error()) - targetContainerState := model.TargetContainerState{ObjectMeta: model.ObjectMeta{Name: targetState.Spec.RootResource, Namespace: targetState.ObjectMeta.Namespace}} + targetContainerState := model.TargetContainerState{ObjectMeta: model.ObjectMeta{Name: targetState.Spec.RootResource, Namespace: namespace}} containerObjectData, _ := json.Marshal(targetContainerState) - err = i.ApiClient.CreateTargetContainer(ctx, targetState.Spec.RootResource, containerObjectData, targetState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + err = i.ApiClient.CreateTargetContainer(ctx, targetState.Spec.RootResource, containerObjectData, namespace, i.Config.User, i.Config.Password) if err != nil { mLog.Errorf("Failed to create target container %s: %s", targetState.Spec.RootResource, err.Error()) return outputs, false, err @@ -313,13 +312,13 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte return outputs, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid catalog name: catalog - %s", name), v1alpha2.BadRequest) } - mLog.Debugf(" P (Materialize Processor): check catalog contains %v, namespace %s", catalogState.Spec.RootResource, catalogState.ObjectMeta.Namespace) - _, err := i.ApiClient.GetCatalogContainer(ctx, catalogState.Spec.RootResource, catalogState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + mLog.Debugf(" P (Materialize Processor): check catalog contains %v, namespace %s", catalogState.Spec.RootResource, namespace) + _, err := i.ApiClient.GetCatalogContainer(ctx, catalogState.Spec.RootResource, namespace, i.Config.User, i.Config.Password) if err != nil && strings.Contains(err.Error(), constants.NotFound) { mLog.Debugf("Failed to get catalog container %s: %s", catalogState.Spec.RootResource, err.Error()) - catalogContainerState := model.CatalogContainerState{ObjectMeta: model.ObjectMeta{Name: catalogState.Spec.RootResource, Namespace: catalogState.ObjectMeta.Namespace}} + catalogContainerState := model.CatalogContainerState{ObjectMeta: model.ObjectMeta{Name: catalogState.Spec.RootResource, Namespace: namespace}} containerObjectData, _ := json.Marshal(catalogContainerState) - err = i.ApiClient.CreateCatalogContainer(ctx, catalogState.Spec.RootResource, containerObjectData, catalogState.ObjectMeta.Namespace, i.Config.User, i.Config.Password) + err = i.ApiClient.CreateCatalogContainer(ctx, catalogState.Spec.RootResource, containerObjectData, namespace, i.Config.User, i.Config.Password) if err != nil { mLog.Errorf("Failed to create catalog container %s: %s", catalogState.Spec.RootResource, err.Error()) return outputs, false, err From 2026d1884fc302507cb895fc6153588ed5658bde Mon Sep 17 00:00:00 2001 From: Lency Qian Date: Fri, 7 Jun 2024 11:32:08 +0800 Subject: [PATCH 4/6] fix --- .../campaign-container-manager.go | 6 +- .../stage/materialize/materialize.go | 20 ++- api/symphony-api.json | 162 +++++------------- 3 files changed, 59 insertions(+), 129 deletions(-) diff --git a/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager.go b/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager.go index fc520b33f..0ec0bb451 100644 --- a/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager.go +++ b/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager.go @@ -55,7 +55,7 @@ func (t *CampaignContainersManager) DeleteState(ctx context.Context, name string ID: name, Metadata: map[string]interface{}{ "namespace": namespace, - "group": model.SolutionGroup, + "group": model.WorkflowGroup, "version": "v1", "resource": "campaigncontainers", "kind": "CampaignContainer", @@ -114,7 +114,7 @@ func (t *CampaignContainersManager) ListState(ctx context.Context, namespace str listRequest := states.ListRequest{ Metadata: map[string]interface{}{ "version": "v1", - "group": model.SolutionGroup, + "group": model.WorkflowGroup, "resource": "campaigncontainers", "namespace": namespace, "kind": "CampaignContainer", @@ -161,7 +161,7 @@ func (t *CampaignContainersManager) GetState(ctx context.Context, id string, nam ID: id, Metadata: map[string]interface{}{ "version": "v1", - "group": model.SolutionGroup, + "group": model.WorkflowGroup, "resource": "campaigncontainers", "namespace": namespace, "kind": "CampaignContainer", diff --git a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go index 0407ccb77..31c97d773 100644 --- a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go +++ b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go @@ -179,7 +179,7 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Debugf(" P (Materialize Processor): check instance contains %v, namespace %s", instanceState.ObjectMeta.Name, namespace) _, err := i.ApiClient.GetInstanceContainer(ctx, instanceState.Spec.RootResource, namespace, i.Config.User, i.Config.Password) if err != nil && strings.Contains(err.Error(), constants.NotFound) { - mLog.Debugf("Failed to get instance container %s: %s", instanceState.Spec.RootResource, err.Error()) + mLog.Debugf("Instance container %s doesn't exist: %s", instanceState.Spec.RootResource, err.Error()) instanceContainerState := model.InstanceContainerState{ObjectMeta: model.ObjectMeta{Name: instanceState.Spec.RootResource, Namespace: namespace}} containerObjectData, _ := json.Marshal(instanceContainerState) err = i.ApiClient.CreateInstanceContainer(ctx, instanceState.Spec.RootResource, containerObjectData, namespace, i.Config.User, i.Config.Password) @@ -187,6 +187,9 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to create instance container %s: %s", instanceState.Spec.RootResource, err.Error()) return outputs, false, err } + } else if err != nil { + mLog.Errorf("Failed to get instance container %s: %s", instanceState.Spec.RootResource, err.Error()) + return outputs, false, err } instanceState.ObjectMeta = updateObjectMeta(instanceState.ObjectMeta, inputs) @@ -224,7 +227,7 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Debugf(" P (Materialize Processor): check solution contains %v, namespace %s", solutionState.Spec.RootResource, namespace) _, err := i.ApiClient.GetSolutionContainer(ctx, solutionState.Spec.RootResource, namespace, i.Config.User, i.Config.Password) if err != nil && strings.Contains(err.Error(), constants.NotFound) { - mLog.Debugf("Failed to get solution container %s: %s", solutionState.Spec.RootResource, err.Error()) + mLog.Debugf("Solution container %s doesn't exist: %s", solutionState.Spec.RootResource, err.Error()) solutionContainerState := model.SolutionContainerState{ObjectMeta: model.ObjectMeta{Name: solutionState.Spec.RootResource, Namespace: namespace}} containerObjectData, _ := json.Marshal(solutionContainerState) err = i.ApiClient.CreateSolutionContainer(ctx, solutionState.Spec.RootResource, containerObjectData, namespace, i.Config.User, i.Config.Password) @@ -232,6 +235,9 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to create solution container %s: %s", solutionState.Spec.RootResource, err.Error()) return outputs, false, err } + } else if err != nil { + mLog.Errorf("Failed to get solution container %s: %s", solutionState.Spec.RootResource, err.Error()) + return outputs, false, err } solutionState.ObjectMeta = updateObjectMeta(solutionState.ObjectMeta, inputs) @@ -269,7 +275,7 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Debugf(" P (Materialize Processor): check target contains %v, namespace %s", targetState.Spec.RootResource, namespace) _, err := i.ApiClient.GetTargetContainer(ctx, targetState.Spec.RootResource, namespace, i.Config.User, i.Config.Password) if err != nil && strings.Contains(err.Error(), constants.NotFound) { - mLog.Debugf("Failed to get target container %s: %s", targetState.Spec.RootResource, err.Error()) + mLog.Debugf("Target container %s doesn't exist: %s", targetState.Spec.RootResource, err.Error()) targetContainerState := model.TargetContainerState{ObjectMeta: model.ObjectMeta{Name: targetState.Spec.RootResource, Namespace: namespace}} containerObjectData, _ := json.Marshal(targetContainerState) err = i.ApiClient.CreateTargetContainer(ctx, targetState.Spec.RootResource, containerObjectData, namespace, i.Config.User, i.Config.Password) @@ -277,6 +283,9 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to create target container %s: %s", targetState.Spec.RootResource, err.Error()) return outputs, false, err } + } else if err != nil { + mLog.Errorf("Failed to get target container %s: %s", targetState.Spec.RootResource, err.Error()) + return outputs, false, err } targetState.ObjectMeta = updateObjectMeta(targetState.ObjectMeta, inputs) @@ -315,7 +324,7 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Debugf(" P (Materialize Processor): check catalog contains %v, namespace %s", catalogState.Spec.RootResource, namespace) _, err := i.ApiClient.GetCatalogContainer(ctx, catalogState.Spec.RootResource, namespace, i.Config.User, i.Config.Password) if err != nil && strings.Contains(err.Error(), constants.NotFound) { - mLog.Debugf("Failed to get catalog container %s: %s", catalogState.Spec.RootResource, err.Error()) + mLog.Debugf("Catalog container %s doesn't exist: %s", catalogState.Spec.RootResource, err.Error()) catalogContainerState := model.CatalogContainerState{ObjectMeta: model.ObjectMeta{Name: catalogState.Spec.RootResource, Namespace: namespace}} containerObjectData, _ := json.Marshal(catalogContainerState) err = i.ApiClient.CreateCatalogContainer(ctx, catalogState.Spec.RootResource, containerObjectData, namespace, i.Config.User, i.Config.Password) @@ -323,6 +332,9 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte mLog.Errorf("Failed to create catalog container %s: %s", catalogState.Spec.RootResource, err.Error()) return outputs, false, err } + } else if err != nil { + mLog.Errorf("Failed to get catalog container %s: %s", catalogState.Spec.RootResource, err.Error()) + return outputs, false, err } catalogState.ObjectMeta = updateObjectMeta(catalogState.ObjectMeta, inputs) diff --git a/api/symphony-api.json b/api/symphony-api.json index f9f9a4531..dd1545ffa 100644 --- a/api/symphony-api.json +++ b/api/symphony-api.json @@ -13,23 +13,17 @@ "lng": "-122.12826" }, "currentSite": { - "baseUrl": "https://symphony-service:8081/v1alpha2/", + "baseUrl": "http://symphony-service:8080/v1alpha2/", "username": "admin", "password": "" - } + } }, "api": { "pubsub": { "shared": true, "provider": { - "type": "providers.pubsub.redis", - "config": { - "name": "redis", - "host": "symphony-redis:6379", - "requireTLS": false, - "password": "", - "numberOfWorkers": 1 - } + "type": "providers.pubsub.memory", + "config": {} } }, "vendors": [ @@ -46,6 +40,7 @@ "catalog": { "type": "providers.config.catalog", "config": { + "baseUrl": "http://symphony-service:8080/v1alpha2/", "user": "admin", "password": "" } @@ -61,10 +56,8 @@ { "name": "stage-manager", "type": "managers.symphony.stage", - "properties": { - "user": "admin", - "password": "", - "providers.state": "k8s-state" + "properties": { + "providers.state": "k8s-state" }, "providers": { "k8s-state": { @@ -105,13 +98,7 @@ } } } - ], - "properties": { - "wait.user": "admin", - "wait.password": "", - "wait.wait.interval": "15", - "wait.wait.count": "10" - } + ] }, { "type": "vendors.activations", @@ -215,11 +202,12 @@ "type": "managers.symphony.jobs", "properties": { "providers.state": "mem-state", + "baseUrl": "http://symphony-service:8080/v1alpha2/", "user": "admin", "password": "", "interval": "#15", "poll.enabled": "false", - "schedule.enabled": "true" + "schedule.enabled": "true" }, "providers": { "mem-state": { @@ -252,14 +240,14 @@ ] }, { - "type": "vendors.targetcontainers", - "route": "targetcontainers", + "type": "vendors.solutions", + "route": "solutions", "managers": [ { - "name": "target-container-manager", - "type": "managers.symphony.targetcontainers", + "name": "solutions-manager", + "type": "managers.symphony.solutions", "properties": { - "providers.state": "k8s-state" + "providers.state": "k8s-state" }, "providers": { "k8s-state": { @@ -273,12 +261,12 @@ ] }, { - "type": "vendors.solutions", - "route": "solutions", + "type": "vendors.instances", + "route": "instances", "managers": [ { - "name": "solutions-manager", - "type": "managers.symphony.solutions", + "name": "instances-manager", + "type": "managers.symphony.instances", "properties": { "providers.state": "k8s-state" }, @@ -294,12 +282,12 @@ ] }, { - "type": "vendors.solutioncontainers", - "route": "solutioncontainers", + "type": "vendors.targetcontainers", + "route": "targetcontainers", "managers": [ { - "name": "solution-container-manager", - "type": "managers.symphony.solutioncontainers", + "name": "target-container-manager", + "type": "managers.symphony.targetcontainers", "properties": { "providers.state": "k8s-state" }, @@ -314,15 +302,15 @@ } ] }, - { - "type": "vendors.instances", - "route": "instances", + { + "type": "vendors.solutioncontainers", + "route": "solutioncontainers", "managers": [ { - "name": "instances-manager", - "type": "managers.symphony.instances", + "name": "solution-container-manager", + "type": "managers.symphony.solutioncontainers", "properties": { - "providers.state": "k8s-state" + "providers.state": "k8s-state" }, "providers": { "k8s-state": { @@ -335,7 +323,7 @@ } ] }, - { + { "type": "vendors.instancecontainers", "route": "instancecontainers", "managers": [ @@ -564,15 +552,15 @@ "properties": { "poll.enabled": "true", "interval": "#15", - "providers.queue": "memory-queue", + "providers.queue": "memeory-queue", "providers.state": "memory-state" }, "providers": { - "memory-queue": { + "memeory-queue": { "type": "providers.queue.memory", "config": {} }, - "memory-state": { + "mem-state": { "type": "providers.state.memory", "config": {} } @@ -583,9 +571,10 @@ "type": "managers.symphony.sync", "properties": { "interval": "#15", - "sync.enabled": "true" , + "sync.enabled": "true", + "baseUrl": "http://symphony-service:8080/v1alpha2/", "user": "admin", - "password": "" + "password": "" } } ] @@ -671,23 +660,7 @@ "type": "bindings.http", "config": { "port": 8080, - "pipeline": [ - { - "type": "middleware.http.tracing", - "properties": { - "pipeline": [ - { - "exporter": { - "type": "tracing.exporters.zipkin", - "backendUrl": "http://symphony-zipkin.default.svc.cluster.local:9411/api/v2/spans", - "sampler": { - "sampleRate": "always" - } - } - } - ] - } - }, + "pipeline": [ { "type": "middleware.http.cors", "properties": { @@ -777,64 +750,9 @@ "port": 8081, "tls": true, "certProvider": { - "type": "certs.localfile", - "config": { - "name": "symphony-serving-cert", - "cert": "/etc/symphony-api/tls/tls.crt", - "key": "/etc/symphony-api/tls/tls.key" - } - }, - "pipeline": [ - { - "type": "middleware.http.tracing", - "properties": { - "pipeline": [ - { - "exporter": { - "type": "tracing.exporters.zipkin", - "backendUrl": "http://symphony-zipkin.default.svc.cluster.local:9411/api/v2/spans", - "sampler": { - "sampleRate": "always" - } - } - } - ] - } - }, - { - "type": "middleware.http.cors", - "properties": { - "Access-Control-Allow-Headers": "authorization,Content-Type", - "Access-Control-Allow-Credentials": "true", - "Access-Control-Allow-Methods": "HEAD,GET,POST,PUT,DELETE,OPTIONS", - "Access-Control-Allow-Origin": "*" - } - }, - { - "type": "middleware.http.jwt", - "properties": { - "ignorePaths": [ - "/v1alpha2/users/auth", - "/v1alpha2/solution/instances", - "/v1alpha2/agent/references", - "/v1alpha2/greetings", - "/v1alpha2/agent/config" - ], - "verifyKey": "SymphonyKey", - "authServer": "kubernetes", - "enableRBAC": false - } - }, - { - "type": "middleware.http.telemetry", - "properties": { - "enabled": true, - "maxBatchSize": 8192, - "maxBatchIntervalSeconds": 2, - "client": "my-dev-machine" - } - } - ] + "type": "certs.autogen", + "config":{} + } } } ] From a26583ef91b6f70c006d73b3b75e3487eafbd711 Mon Sep 17 00:00:00 2001 From: Lency Qian Date: Fri, 7 Jun 2024 11:51:32 +0800 Subject: [PATCH 5/6] fix comments --- .../campaigncontainers/campaign-container-manager_test.go | 2 +- .../catalogcontainers/catalog-container-manager_test.go | 4 ++-- .../instancecontainers/instance-container-manager_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager_test.go b/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager_test.go index 6ee8f32b4..a0ff19212 100644 --- a/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/campaigncontainers/campaign-container-manager_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" ) -// write test case to create a CampaignSpec using the manager +// write test case to create a CampaignContainerSpec using the manager func TestCreateGetDeleteCampaignContainersState(t *testing.T) { stateProvider := &memorystate.MemoryStateProvider{} stateProvider.Init(memorystate.MemoryStateProviderConfig{}) diff --git a/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager_test.go b/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager_test.go index e5a61311c..c25f81f5c 100644 --- a/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/catalogcontainers/catalog-container-manager_test.go @@ -15,8 +15,8 @@ import ( "github.com/stretchr/testify/assert" ) -// write test case to create a CampaignSpec using the manager -func TestCreateGetDeleteCampaignContainersState(t *testing.T) { +// write test case to create a CatalogContainerSpec using the manager +func TestCreateGetDeleteCatalogContainersState(t *testing.T) { stateProvider := &memorystate.MemoryStateProvider{} stateProvider.Init(memorystate.MemoryStateProviderConfig{}) manager := CatalogContainersManager{ diff --git a/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager_test.go b/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager_test.go index 686f6da91..33ac74caa 100644 --- a/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager_test.go +++ b/api/pkg/apis/v1alpha1/managers/instancecontainers/instance-container-manager_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/assert" ) -// write test case to create a InstanceSpec using the manager +// write test case to create a InstanceContainerSpec using the manager func TestCreateGetDeleteInstanceContainersState(t *testing.T) { stateProvider := &memorystate.MemoryStateProvider{} stateProvider.Init(memorystate.MemoryStateProviderConfig{}) From 29e474c42dbb82b4e65280b0c106ee7f56184de1 Mon Sep 17 00:00:00 2001 From: Lency Qian Date: Fri, 7 Jun 2024 20:20:20 +0800 Subject: [PATCH 6/6] container webhook --- docs/samples/k8s/hello-world/instance.yaml | 1 + k8s/apis/fabric/v1/target_webhook.go | 4 + k8s/apis/fabric/v1/targetcontainer_webhook.go | 122 +++++++++++ k8s/apis/federation/v1/catalog_webhook.go | 8 +- .../federation/v1/catalogcontainer_webhook.go | 122 +++++++++++ k8s/apis/solution/v1/instance_webhook.go | 12 +- .../solution/v1/instancecontainer_webhook.go | 122 +++++++++++ k8s/apis/solution/v1/solution_webhook.go | 5 + .../solution/v1/solutioncontainer_webhook.go | 122 +++++++++++ k8s/apis/workflow/v1/campaign_webhook.go | 5 +- .../workflow/v1/campaigncontainer_webhook.go | 122 +++++++++++ k8s/config/oss/webhook/manifests.yaml | 205 ++++++++++++++++++ k8s/main.go | 20 ++ .../templates/symphony-core/symphonyk8s.yaml | 205 ++++++++++++++++++ 14 files changed, 1068 insertions(+), 7 deletions(-) create mode 100644 k8s/apis/fabric/v1/targetcontainer_webhook.go create mode 100644 k8s/apis/federation/v1/catalogcontainer_webhook.go create mode 100644 k8s/apis/solution/v1/instancecontainer_webhook.go create mode 100644 k8s/apis/solution/v1/solutioncontainer_webhook.go create mode 100644 k8s/apis/workflow/v1/campaigncontainer_webhook.go diff --git a/docs/samples/k8s/hello-world/instance.yaml b/docs/samples/k8s/hello-world/instance.yaml index e6bb6f09f..f22e05c4b 100644 --- a/docs/samples/k8s/hello-world/instance.yaml +++ b/docs/samples/k8s/hello-world/instance.yaml @@ -3,6 +3,7 @@ kind: Instance metadata: name: sampleprometheusinstance-v1 spec: + rootResource: sampleprometheusinstance scope: sample-k8s-scope solution: sampleprometheusserver:v1 target: diff --git a/k8s/apis/fabric/v1/target_webhook.go b/k8s/apis/fabric/v1/target_webhook.go index ca97f3be4..65ca3a09a 100644 --- a/k8s/apis/fabric/v1/target_webhook.go +++ b/k8s/apis/fabric/v1/target_webhook.go @@ -41,6 +41,10 @@ func (r *Target) SetupWebhookWithManager(mgr ctrl.Manager) error { target := rawObj.(*Target) return []string{target.Spec.DisplayName} }) + mgr.GetFieldIndexer().IndexField(context.Background(), &Target{}, ".spec.rootResource", func(rawObj client.Object) []string { + target := rawObj.(*Target) + return []string{target.Spec.RootResource} + }) dict, _ := configutils.GetValidationPoilicies() if v, ok := dict["target"]; ok { diff --git a/k8s/apis/fabric/v1/targetcontainer_webhook.go b/k8s/apis/fabric/v1/targetcontainer_webhook.go new file mode 100644 index 000000000..c9cec6ca4 --- /dev/null +++ b/k8s/apis/fabric/v1/targetcontainer_webhook.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package v1 + +import ( + "context" + "fmt" + "gopls-workspace/apis/metrics/v1" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var targetcontainerlog = logf.Log.WithName("targetcontainer-resource") +var myTargetContainerClient client.Client +var targetContainerWebhookValidationMetrics *metrics.Metrics + +func (r *TargetContainer) SetupWebhookWithManager(mgr ctrl.Manager) error { + myTargetContainerClient = mgr.GetClient() + mgr.GetFieldIndexer().IndexField(context.Background(), &TargetContainer{}, ".metadata.name", func(rawObj client.Object) []string { + target := rawObj.(*TargetContainer) + return []string{target.Name} + }) + + // initialize the controller operation metrics + if targetContainerWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + targetContainerWebhookValidationMetrics = metrics + } + + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-fabric-symphony-v1-targetcontainer,mutating=true,failurePolicy=fail,sideEffects=None,groups=fabric.symphony,resources=targetcontainers,verbs=create;update,versions=v1,name=mtargetcontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &TargetContainer{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *TargetContainer) Default() { + targetcontainerlog.Info("default", "name", r.Name) +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. + +//+kubebuilder:webhook:path=/validate-fabric-symphony-v1-targetcontainer,mutating=false,failurePolicy=fail,sideEffects=None,groups=fabric.symphony,resources=targetcontainers,verbs=create;update;delete,versions=v1,name=vtargetcontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &TargetContainer{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *TargetContainer) ValidateCreate() (admission.Warnings, error) { + targetcontainerlog.Info("validate create", "name", r.Name) + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *TargetContainer) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + targetcontainerlog.Info("validate update", "name", r.Name) + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *TargetContainer) ValidateDelete() (admission.Warnings, error) { + targetcontainerlog.Info("validate delete", "name", r.Name) + + validateDeleteTime := time.Now() + validationError := r.validateDeleteTargetContainer() + if validationError != nil { + targetContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.CatalogResourceType) + } else { + targetContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.CatalogResourceType) + } + + return nil, validationError +} + +func (r *TargetContainer) validateDeleteTargetContainer() error { + return r.validateTargets() +} + +func (r *TargetContainer) validateTargets() error { + var target TargetList + err := myTargetContainerClient.List(context.Background(), &target, client.InNamespace(r.Namespace), client.MatchingFields{".spec.rootResource": r.Name}) + if err != nil { + targetcontainerlog.Error(err, "could not list targets", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("could not list targets for target container %s.", r.Name)) + } + + if len(target.Items) != 0 { + targetcontainerlog.Error(err, "targets are not empty", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("targets with root resource '%s' are not empty", r.Name)) + } + + return nil +} diff --git a/k8s/apis/federation/v1/catalog_webhook.go b/k8s/apis/federation/v1/catalog_webhook.go index c82724527..5c59391c7 100644 --- a/k8s/apis/federation/v1/catalog_webhook.go +++ b/k8s/apis/federation/v1/catalog_webhook.go @@ -34,8 +34,12 @@ var catalogWebhookValidationMetrics *metrics.Metrics func (r *Catalog) SetupWebhookWithManager(mgr ctrl.Manager) error { myCatalogClient = mgr.GetClient() mgr.GetFieldIndexer().IndexField(context.Background(), &Catalog{}, ".metadata.name", func(rawObj client.Object) []string { - target := rawObj.(*Catalog) - return []string{target.Name} + catalog := rawObj.(*Catalog) + return []string{catalog.Name} + }) + mgr.GetFieldIndexer().IndexField(context.Background(), &Catalog{}, ".spec.rootResource", func(rawObj client.Object) []string { + catalog := rawObj.(*Catalog) + return []string{catalog.Spec.RootResource} }) // initialize the controller operation metrics diff --git a/k8s/apis/federation/v1/catalogcontainer_webhook.go b/k8s/apis/federation/v1/catalogcontainer_webhook.go new file mode 100644 index 000000000..a2a05f88a --- /dev/null +++ b/k8s/apis/federation/v1/catalogcontainer_webhook.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package v1 + +import ( + "context" + "fmt" + "gopls-workspace/apis/metrics/v1" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var catalogcontainerlog = logf.Log.WithName("catalogcontainer-resource") +var myCatalogContainerClient client.Client +var catalogContainerWebhookValidationMetrics *metrics.Metrics + +func (r *CatalogContainer) SetupWebhookWithManager(mgr ctrl.Manager) error { + myCatalogContainerClient = mgr.GetClient() + mgr.GetFieldIndexer().IndexField(context.Background(), &CatalogContainer{}, ".metadata.name", func(rawObj client.Object) []string { + catalog := rawObj.(*CatalogContainer) + return []string{catalog.Name} + }) + + // initialize the controller operation metrics + if catalogContainerWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + catalogContainerWebhookValidationMetrics = metrics + } + + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-federation-symphony-v1-catalogcontainer,mutating=true,failurePolicy=fail,sideEffects=None,groups=federation.symphony,resources=catalogcontainers,verbs=create;update,versions=v1,name=mcatalogcontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &CatalogContainer{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *CatalogContainer) Default() { + catalogcontainerlog.Info("default", "name", r.Name) +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. + +//+kubebuilder:webhook:path=/validate-federation-symphony-v1-catalogcontainer,mutating=false,failurePolicy=fail,sideEffects=None,groups=federation.symphony,resources=catalogcontainers,verbs=create;update;delete,versions=v1,name=vcatalogcontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &CatalogContainer{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *CatalogContainer) ValidateCreate() (admission.Warnings, error) { + catalogcontainerlog.Info("validate create", "name", r.Name) + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *CatalogContainer) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + catalogcontainerlog.Info("validate update", "name", r.Name) + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *CatalogContainer) ValidateDelete() (admission.Warnings, error) { + catalogcontainerlog.Info("validate delete", "name", r.Name) + + validateDeleteTime := time.Now() + validationError := r.validateDeleteCatalogContainer() + if validationError != nil { + catalogContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.CatalogResourceType) + } else { + catalogContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.CatalogResourceType) + } + + return nil, validationError +} + +func (r *CatalogContainer) validateDeleteCatalogContainer() error { + return r.validateCatalogs() +} + +func (r *CatalogContainer) validateCatalogs() error { + var catalog CatalogList + err := myCatalogContainerClient.List(context.Background(), &catalog, client.InNamespace(r.Namespace), client.MatchingFields{".spec.rootResource": r.Name}) + if err != nil { + catalogcontainerlog.Error(err, "could not list catalogs", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("could not list catalogs for catalog container %s.", r.Name)) + } + + if len(catalog.Items) != 0 { + catalogcontainerlog.Error(err, "catalogs are not empty", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("catalogs with root resource '%s' are not empty", r.Name)) + } + + return nil +} diff --git a/k8s/apis/solution/v1/instance_webhook.go b/k8s/apis/solution/v1/instance_webhook.go index c6a8eea77..3c2bd2268 100644 --- a/k8s/apis/solution/v1/instance_webhook.go +++ b/k8s/apis/solution/v1/instance_webhook.go @@ -34,12 +34,16 @@ var instanceWebhookValidationMetrics *metrics.Metrics func (r *Instance) SetupWebhookWithManager(mgr ctrl.Manager) error { myInstanceClient = mgr.GetClient() mgr.GetFieldIndexer().IndexField(context.Background(), &Instance{}, "spec.displayName", func(rawObj client.Object) []string { - target := rawObj.(*Instance) - return []string{target.Spec.DisplayName} + instance := rawObj.(*Instance) + return []string{instance.Spec.DisplayName} }) mgr.GetFieldIndexer().IndexField(context.Background(), &Instance{}, "spec.solution", func(rawObj client.Object) []string { - target := rawObj.(*Instance) - return []string{target.Spec.Solution} + instance := rawObj.(*Instance) + return []string{instance.Spec.Solution} + }) + mgr.GetFieldIndexer().IndexField(context.Background(), &Instance{}, ".spec.rootResource", func(rawObj client.Object) []string { + instance := rawObj.(*Instance) + return []string{instance.Spec.RootResource} }) // initialize the controller operation metrics diff --git a/k8s/apis/solution/v1/instancecontainer_webhook.go b/k8s/apis/solution/v1/instancecontainer_webhook.go new file mode 100644 index 000000000..0c74d6aee --- /dev/null +++ b/k8s/apis/solution/v1/instancecontainer_webhook.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package v1 + +import ( + "context" + "fmt" + "gopls-workspace/apis/metrics/v1" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var instancecontainerlog = logf.Log.WithName("instancecontainer-resource") +var myInstanceContainerClient client.Client +var instanceContainerWebhookValidationMetrics *metrics.Metrics + +func (r *InstanceContainer) SetupWebhookWithManager(mgr ctrl.Manager) error { + myInstanceContainerClient = mgr.GetClient() + mgr.GetFieldIndexer().IndexField(context.Background(), &InstanceContainer{}, ".metadata.name", func(rawObj client.Object) []string { + instance := rawObj.(*InstanceContainer) + return []string{instance.Name} + }) + + // initialize the controller operation metrics + if instanceContainerWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + instanceContainerWebhookValidationMetrics = metrics + } + + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-solution-symphony-v1-instancecontainer,mutating=true,failurePolicy=fail,sideEffects=None,groups=solution.symphony,resources=instancecontainers,verbs=create;update,versions=v1,name=minstancecontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &InstanceContainer{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *InstanceContainer) Default() { + instancecontainerlog.Info("default", "name", r.Name) +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. + +//+kubebuilder:webhook:path=/validate-solution-symphony-v1-instancecontainer,mutating=false,failurePolicy=fail,sideEffects=None,groups=solution.symphony,resources=instancecontainers,verbs=create;update;delete,versions=v1,name=vinstancecontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &InstanceContainer{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *InstanceContainer) ValidateCreate() (admission.Warnings, error) { + instancecontainerlog.Info("validate create", "name", r.Name) + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *InstanceContainer) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + instancecontainerlog.Info("validate update", "name", r.Name) + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *InstanceContainer) ValidateDelete() (admission.Warnings, error) { + instancecontainerlog.Info("validate delete", "name", r.Name) + + validateDeleteTime := time.Now() + validationError := r.validateDeleteInstanceContainer() + if validationError != nil { + instanceContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.CatalogResourceType) + } else { + instanceContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.CatalogResourceType) + } + + return nil, validationError +} + +func (r *InstanceContainer) validateDeleteInstanceContainer() error { + return r.validateInstances() +} + +func (r *InstanceContainer) validateInstances() error { + var instance InstanceList + err := myInstanceContainerClient.List(context.Background(), &instance, client.InNamespace(r.Namespace), client.MatchingFields{".spec.rootResource": r.Name}) + if err != nil { + instancecontainerlog.Error(err, "could not list instances", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("could not list instances for instance container %s.", r.Name)) + } + + if len(instance.Items) != 0 { + instancecontainerlog.Error(err, "instances are not empty", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("instances with root resource '%s' are not empty", r.Name)) + } + + return nil +} diff --git a/k8s/apis/solution/v1/solution_webhook.go b/k8s/apis/solution/v1/solution_webhook.go index 06867445f..20f617690 100644 --- a/k8s/apis/solution/v1/solution_webhook.go +++ b/k8s/apis/solution/v1/solution_webhook.go @@ -33,6 +33,11 @@ func (r *Solution) SetupWebhookWithManager(mgr ctrl.Manager) error { solution := rawObj.(*Solution) return []string{solution.Spec.DisplayName} }) + mgr.GetFieldIndexer().IndexField(context.Background(), &Solution{}, ".spec.rootResource", func(rawObj client.Object) []string { + solution := rawObj.(*Solution) + return []string{solution.Spec.RootResource} + }) + return ctrl.NewWebhookManagedBy(mgr). For(r). Complete() diff --git a/k8s/apis/solution/v1/solutioncontainer_webhook.go b/k8s/apis/solution/v1/solutioncontainer_webhook.go new file mode 100644 index 000000000..76c5414fb --- /dev/null +++ b/k8s/apis/solution/v1/solutioncontainer_webhook.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package v1 + +import ( + "context" + "fmt" + "gopls-workspace/apis/metrics/v1" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var solutioncontainerlog = logf.Log.WithName("solutioncontainer-resource") +var mySolutionContainerClient client.Client +var solutionContainerWebhookValidationMetrics *metrics.Metrics + +func (r *SolutionContainer) SetupWebhookWithManager(mgr ctrl.Manager) error { + mySolutionContainerClient = mgr.GetClient() + mgr.GetFieldIndexer().IndexField(context.Background(), &SolutionContainer{}, ".metadata.name", func(rawObj client.Object) []string { + solution := rawObj.(*SolutionContainer) + return []string{solution.Name} + }) + + // initialize the controller operation metrics + if solutionContainerWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + solutionContainerWebhookValidationMetrics = metrics + } + + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-solution-symphony-v1-solutioncontainer,mutating=true,failurePolicy=fail,sideEffects=None,groups=solution.symphony,resources=solutioncontainers,verbs=create;update,versions=v1,name=msolutioncontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &SolutionContainer{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *SolutionContainer) Default() { + solutioncontainerlog.Info("default", "name", r.Name) +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. + +//+kubebuilder:webhook:path=/validate-solution-symphony-v1-solutioncontainer,mutating=false,failurePolicy=fail,sideEffects=None,groups=solution.symphony,resources=solutioncontainers,verbs=create;update;delete,versions=v1,name=vsolutioncontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &SolutionContainer{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *SolutionContainer) ValidateCreate() (admission.Warnings, error) { + solutioncontainerlog.Info("validate create", "name", r.Name) + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *SolutionContainer) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + solutioncontainerlog.Info("validate update", "name", r.Name) + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *SolutionContainer) ValidateDelete() (admission.Warnings, error) { + solutioncontainerlog.Info("validate delete", "name", r.Name) + + validateDeleteTime := time.Now() + validationError := r.validateDeleteSolutionContainer() + if validationError != nil { + solutionContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.CatalogResourceType) + } else { + solutionContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.CatalogResourceType) + } + + return nil, validationError +} + +func (r *SolutionContainer) validateDeleteSolutionContainer() error { + return r.validateSolutions() +} + +func (r *SolutionContainer) validateSolutions() error { + var solution SolutionList + err := mySolutionContainerClient.List(context.Background(), &solution, client.InNamespace(r.Namespace), client.MatchingFields{".spec.rootResource": r.Name}) + if err != nil { + solutioncontainerlog.Error(err, "could not list solutions", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("could not list solutions for solution container %s.", r.Name)) + } + + if len(solution.Items) != 0 { + solutioncontainerlog.Error(err, "solutions are not empty", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("solutions with root resource '%s' are not empty", r.Name)) + } + + return nil +} diff --git a/k8s/apis/workflow/v1/campaign_webhook.go b/k8s/apis/workflow/v1/campaign_webhook.go index 16047f51d..8eb352f12 100644 --- a/k8s/apis/workflow/v1/campaign_webhook.go +++ b/k8s/apis/workflow/v1/campaign_webhook.go @@ -35,6 +35,10 @@ func (r *Campaign) SetupWebhookWithManager(mgr ctrl.Manager) error { campaign := rawObj.(*Campaign) return []string{campaign.Name} }) + mgr.GetFieldIndexer().IndexField(context.Background(), &Campaign{}, ".spec.rootResource", func(rawObj client.Object) []string { + campaign := rawObj.(*Campaign) + return []string{campaign.Spec.RootResource} + }) // initialize the controller operation metrics if catalogWebhookValidationMetrics == nil { @@ -151,7 +155,6 @@ func (r *Campaign) validateRootResource() *field.Error { return field.Invalid(field.NewPath("spec").Child("rootResource"), r.Spec.RootResource, "rootResource must be a valid campaign container") } - campaignlog.Info("validate OwnerReferences", "name", r.Name) if len(r.ObjectMeta.OwnerReferences) == 0 { return field.Invalid(field.NewPath("metadata").Child("ownerReference"), len(r.ObjectMeta.OwnerReferences), "ownerReference must be set") } diff --git a/k8s/apis/workflow/v1/campaigncontainer_webhook.go b/k8s/apis/workflow/v1/campaigncontainer_webhook.go new file mode 100644 index 000000000..79a3359bb --- /dev/null +++ b/k8s/apis/workflow/v1/campaigncontainer_webhook.go @@ -0,0 +1,122 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package v1 + +import ( + "context" + "fmt" + "gopls-workspace/apis/metrics/v1" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var campaigncontainerlog = logf.Log.WithName("campaigncontainer-resource") +var myCampaignContainerClient client.Client +var campaignContainerWebhookValidationMetrics *metrics.Metrics + +func (r *CampaignContainer) SetupWebhookWithManager(mgr ctrl.Manager) error { + myCampaignContainerClient = mgr.GetClient() + mgr.GetFieldIndexer().IndexField(context.Background(), &CampaignContainer{}, ".metadata.name", func(rawObj client.Object) []string { + campaign := rawObj.(*CampaignContainer) + return []string{campaign.Name} + }) + + // initialize the controller operation metrics + if campaignContainerWebhookValidationMetrics == nil { + metrics, err := metrics.New() + if err != nil { + return err + } + campaignContainerWebhookValidationMetrics = metrics + } + + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-workflow-symphony-v1-campaigncontainer,mutating=true,failurePolicy=fail,sideEffects=None,groups=workflow.symphony,resources=campaigncontainers,verbs=create;update,versions=v1,name=mcampaigncontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &CampaignContainer{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *CampaignContainer) Default() { + campaigncontainerlog.Info("default", "name", r.Name) +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. + +//+kubebuilder:webhook:path=/validate-workflow-symphony-v1-campaigncontainer,mutating=false,failurePolicy=fail,sideEffects=None,groups=workflow.symphony,resources=campaigncontainers,verbs=create;update;delete,versions=v1,name=vcampaigncontainer.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &CampaignContainer{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *CampaignContainer) ValidateCreate() (admission.Warnings, error) { + campaigncontainerlog.Info("validate create", "name", r.Name) + + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *CampaignContainer) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { + campaigncontainerlog.Info("validate update", "name", r.Name) + + return nil, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *CampaignContainer) ValidateDelete() (admission.Warnings, error) { + campaigncontainerlog.Info("validate delete", "name", r.Name) + + validateDeleteTime := time.Now() + validationError := r.validateDeleteCampaignContainer() + if validationError != nil { + campaignContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.InvalidResource, + metrics.CatalogResourceType) + } else { + campaignContainerWebhookValidationMetrics.ControllerValidationLatency( + validateDeleteTime, + metrics.CreateOperationType, + metrics.ValidResource, + metrics.CatalogResourceType) + } + + return nil, validationError +} + +func (r *CampaignContainer) validateDeleteCampaignContainer() error { + return r.validateCampaigns() +} + +func (r *CampaignContainer) validateCampaigns() error { + var campaign CampaignList + err := myCampaignContainerClient.List(context.Background(), &campaign, client.InNamespace(r.Namespace), client.MatchingFields{".spec.rootResource": r.Name}) + if err != nil { + campaigncontainerlog.Error(err, "could not list campaigns", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("could not list campaigns for campaign container %s.", r.Name)) + } + + if len(campaign.Items) != 0 { + campaigncontainerlog.Error(err, "campaigns are not empty", "name", r.Name) + return apierrors.NewBadRequest(fmt.Sprintf("campaigns with root resource '%s' are not empty", r.Name)) + } + + return nil +} diff --git a/k8s/config/oss/webhook/manifests.yaml b/k8s/config/oss/webhook/manifests.yaml index 0f97a9cc7..89adaa190 100644 --- a/k8s/config/oss/webhook/manifests.yaml +++ b/k8s/config/oss/webhook/manifests.yaml @@ -24,6 +24,26 @@ webhooks: resources: - targets sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-fabric-symphony-v1-targetcontainer + failurePolicy: Fail + name: mtargetcontainer.kb.io + rules: + - apiGroups: + - fabric.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - targetcontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -44,6 +64,26 @@ webhooks: resources: - instances sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-solution-symphony-v1-instancecontainer + failurePolicy: Fail + name: minstancecontainer.kb.io + rules: + - apiGroups: + - solution.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - instancecontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -64,6 +104,26 @@ webhooks: resources: - solutions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-solution-symphony-v1-solutioncontainer + failurePolicy: Fail + name: msolutioncontainer.kb.io + rules: + - apiGroups: + - solution.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - solutioncontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -84,6 +144,26 @@ webhooks: resources: - campaigns sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-workflow-symphony-v1-campaigncontainer + failurePolicy: Fail + name: mcampaigncontainer.kb.io + rules: + - apiGroups: + - workflow.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - campaigncontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -104,6 +184,26 @@ webhooks: resources: - catalogs sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-federation-symphony-v1-catalogcontainer + failurePolicy: Fail + name: mcatalogcontainer.kb.io + rules: + - apiGroups: + - federation.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - catalogcontainers + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -130,6 +230,27 @@ webhooks: resources: - targets sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-fabric-symphony-v1-targetcontainer + failurePolicy: Fail + name: vtargetcontainer.kb.io + rules: + - apiGroups: + - fabric.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - targetcontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -150,6 +271,27 @@ webhooks: resources: - instances sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-solution-symphony-v1-instancecontainer + failurePolicy: Fail + name: vinstancecontainer.kb.io + rules: + - apiGroups: + - solution.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - instancecontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -170,6 +312,27 @@ webhooks: resources: - solutions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-solution-symphony-v1-solutioncontainer + failurePolicy: Fail + name: vsolutioncontainer.kb.io + rules: + - apiGroups: + - solution.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - solutioncontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -190,6 +353,27 @@ webhooks: resources: - campaigns sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-workflow-symphony-v1-campaigncontainer + failurePolicy: Fail + name: vcampaigncontainer.kb.io + rules: + - apiGroups: + - workflow.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - campaigncontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -210,3 +394,24 @@ webhooks: resources: - catalogs sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-federation-symphony-v1-catalogcontainer + failurePolicy: Fail + name: vcatalogcontainer.kb.io + rules: + - apiGroups: + - federation.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - catalogcontainers + sideEffects: None diff --git a/k8s/main.go b/k8s/main.go index aac9b1c8f..804594757 100644 --- a/k8s/main.go +++ b/k8s/main.go @@ -302,6 +302,26 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "Campaign") os.Exit(1) } + if err = (&workflowv1.CampaignContainer{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "CampaignContainer") + os.Exit(1) + } + if err = (&federationv1.CatalogContainer{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "CatalogContainer") + os.Exit(1) + } + if err = (&solutionv1.InstanceContainer{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "InstanceContainer") + os.Exit(1) + } + if err = (&solutionv1.SolutionContainer{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "SolutionContainer") + os.Exit(1) + } + if err = (&fabricv1.TargetContainer{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "TargetContainer") + os.Exit(1) + } } if err = (&solutioncontrollers.SolutionContainerReconciler{ Client: mgr.GetClient(), diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index fcdb48a51..cb85fcc57 100644 --- a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -2665,6 +2665,26 @@ webhooks: resources: - targets sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-fabric-symphony-v1-targetcontainer + failurePolicy: Fail + name: mtargetcontainer.kb.io + rules: + - apiGroups: + - fabric.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - targetcontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2685,6 +2705,26 @@ webhooks: resources: - instances sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-solution-symphony-v1-instancecontainer + failurePolicy: Fail + name: minstancecontainer.kb.io + rules: + - apiGroups: + - solution.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - instancecontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2705,6 +2745,26 @@ webhooks: resources: - solutions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-solution-symphony-v1-solutioncontainer + failurePolicy: Fail + name: msolutioncontainer.kb.io + rules: + - apiGroups: + - solution.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - solutioncontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2725,6 +2785,26 @@ webhooks: resources: - campaigns sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-workflow-symphony-v1-campaigncontainer + failurePolicy: Fail + name: mcampaigncontainer.kb.io + rules: + - apiGroups: + - workflow.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - campaigncontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2745,6 +2825,26 @@ webhooks: resources: - catalogs sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /mutate-federation-symphony-v1-catalogcontainer + failurePolicy: Fail + name: mcatalogcontainer.kb.io + rules: + - apiGroups: + - federation.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - catalogcontainers + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -2774,6 +2874,27 @@ webhooks: resources: - targets sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-fabric-symphony-v1-targetcontainer + failurePolicy: Fail + name: vtargetcontainer.kb.io + rules: + - apiGroups: + - fabric.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - targetcontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2794,6 +2915,27 @@ webhooks: resources: - instances sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-solution-symphony-v1-instancecontainer + failurePolicy: Fail + name: vinstancecontainer.kb.io + rules: + - apiGroups: + - solution.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - instancecontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2814,6 +2956,27 @@ webhooks: resources: - solutions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-solution-symphony-v1-solutioncontainer + failurePolicy: Fail + name: vsolutioncontainer.kb.io + rules: + - apiGroups: + - solution.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - solutioncontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2834,6 +2997,27 @@ webhooks: resources: - campaigns sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-workflow-symphony-v1-campaigncontainer + failurePolicy: Fail + name: vcampaigncontainer.kb.io + rules: + - apiGroups: + - workflow.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - campaigncontainers + sideEffects: None - admissionReviewVersions: - v1 clientConfig: @@ -2854,3 +3038,24 @@ webhooks: resources: - catalogs sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: '{{ include "symphony.fullname" . }}-webhook-service' + namespace: '{{ .Release.Namespace }}' + path: /validate-federation-symphony-v1-catalogcontainer + failurePolicy: Fail + name: vcatalogcontainer.kb.io + rules: + - apiGroups: + - federation.symphony + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - catalogcontainers + sideEffects: None