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..0ec0bb451 --- /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.WorkflowGroup, + "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.WorkflowGroup, + "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.WorkflowGroup, + "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..a0ff19212 --- /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 CampaignContainerSpec 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 a47c7f6af..43eb89098 100644 --- a/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go +++ b/api/pkg/apis/v1alpha1/managers/campaigns/campaigns-manager.go @@ -155,13 +155,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/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..c25f81f5c --- /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 CatalogContainerSpec using the manager +func TestCreateGetDeleteCatalogContainersState(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/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..33ac74caa --- /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 InstanceContainerSpec 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/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/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/providers/stage/materialize/materialize.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go index 60f33f7e6..31c97d773 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" @@ -159,11 +160,39 @@ 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) + } + + 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("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) + if err != nil { + 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, 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 +208,39 @@ 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) + } + + 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("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) + if err != nil { + 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) 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 +256,39 @@ 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) + } + + 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("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) + if err != nil { + 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) 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 +305,39 @@ 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) + } + + 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("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) + if err != nil { + 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) 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 +357,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/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..dd1545ffa 100644 --- a/api/symphony-api.json +++ b/api/symphony-api.json @@ -166,6 +166,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", @@ -260,6 +281,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", @@ -521,6 +605,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/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..f22e05c4b 100644 --- a/docs/samples/k8s/hello-world/instance.yaml +++ b/docs/samples/k8s/hello-world/instance.yaml @@ -1,9 +1,10 @@ apiVersion: solution.symphony/v1 kind: Instance metadata: - name: sample-prometheus-instance-v1 + name: sampleprometheusinstance-v1 spec: + rootResource: sampleprometheusinstance 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..65ca3a09a 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" @@ -40,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 { @@ -81,6 +86,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 +183,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 +266,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/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 8db02b804..5c59391c7 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" @@ -31,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 @@ -58,6 +65,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 +146,26 @@ func (r *Catalog) ValidateDelete() (admission.Warnings, error) { } func (r *Catalog) validateCreateCatalog() error { - return r.checkSchema() -} + var allErrs field.ErrorList + + 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 + } -func (r *Catalog) checkSchema() error { + 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 +173,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 +181,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 +189,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 +215,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/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 3de369ccf..3c2bd2268 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. @@ -33,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 @@ -72,6 +77,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 +171,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 +246,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/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 ec32fa594..20f617690 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,9 +30,14 @@ 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} }) + 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() @@ -47,6 +56,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 +105,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 +135,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/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 new file mode 100644 index 000000000..8eb352f12 --- /dev/null +++ b/k8s/apis/workflow/v1/campaign_webhook.go @@ -0,0 +1,163 @@ +/* + * 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} + }) + 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 { + 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") + } + + 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/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 ffd43d4d3..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,66 @@ 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: + 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: + 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: @@ -84,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 @@ -110,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: @@ -130,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: @@ -150,6 +312,68 @@ 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: + 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: + 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: @@ -170,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/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..804594757 100644 --- a/k8s/main.go +++ b/k8s/main.go @@ -298,6 +298,30 @@ 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 = (&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/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", diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index ee128f5b4..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,66 @@ 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: + 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: + 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: @@ -2725,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 @@ -2754,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: @@ -2774,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: @@ -2794,6 +2956,68 @@ 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: + 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: + 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: @@ -2814,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 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,