diff --git a/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go b/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go index 1e40649a2..502621553 100644 --- a/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go +++ b/api/pkg/apis/v1alpha1/managers/solution/solution-manager.go @@ -534,7 +534,13 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy // } // } + defaultScope := deployment.Instance.Spec.Scope + // ensure to restore the original scope defined in instance in case the scope is changed during the deployment + defer func() { + deployment.Instance.Spec.Scope = defaultScope + }() for i := 0; i < retryCount; i++ { + deployment.Instance.Spec.Scope = getCurrentApplicationScope(ctx, deployment.Instance, deployment.Targets[step.Target]) componentResults, stepError = (provider.(tgt.ITargetProvider)).Apply(ctx, dep, step, deployment.IsDryRun) if stepError == nil { targetResult[step.Target] = 1 @@ -554,6 +560,7 @@ func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.Deploy summary.UpdateTargetResult(step.Target, model.TargetResultSpec{Status: targetResultStatus, Message: targetResultMessage, ComponentResults: componentResults}) // TODO: this keeps only the last error on the target time.Sleep(5 * time.Second) //TODO: make this configurable? } + deployment.Instance.Spec.Scope = defaultScope } if stepError != nil { log.ErrorfCtx(ctx, " M (Solution): failed to execute deployment step: %+v", stepError) @@ -773,6 +780,7 @@ func (s *SolutionManager) Get(ctx context.Context, deployment model.DeploymentSp ret = state ret.TargetComponent = make(map[string]string) retComponents := make([]model.ComponentSpec, 0) + defaultScope := deployment.Instance.Spec.Scope for _, step := range plan.Steps { if s.IsTarget && !api_utils.ContainsString(s.TargetNames, step.Target) { @@ -783,6 +791,7 @@ func (s *SolutionManager) Get(ctx context.Context, deployment model.DeploymentSp } deployment.ActiveTarget = step.Target + deployment.Instance.Spec.Scope = getCurrentApplicationScope(ctx, deployment.Instance, deployment.Targets[step.Target]) var override tgt.ITargetProvider role := step.Role @@ -828,6 +837,7 @@ func (s *SolutionManager) Get(ctx context.Context, deployment model.DeploymentSp retComponents = append(retComponents, c) } } + deployment.Instance.Spec.Scope = defaultScope } ret.Components = retComponents return ret, retComponents, nil @@ -888,6 +898,22 @@ func (s *SolutionManager) Reconcil() []error { return nil } +func getCurrentApplicationScope(ctx context.Context, instance model.InstanceState, target model.TargetState) string { + log.InfofCtx(ctx, " M (Solution): getting current application scope, instance scope: %s, target application scope: %s", instance.Spec.Scope, target.Spec.SolutionScope) + if instance.Spec.Scope == "" { + if target.Spec.SolutionScope == "" { + return "default" + } + return target.Spec.SolutionScope + } + if target.Spec.SolutionScope != "" && target.Spec.SolutionScope != instance.Spec.Scope { + message := fmt.Sprintf(" M (Solution): target application scope: %s is inconsistent with instance scope: %s", target.Spec.SolutionScope, instance.Spec.Scope) + log.WarnfCtx(ctx, message) + observ_utils.EmitUserAuditsLogs(ctx, message) + } + return instance.Spec.Scope +} + func findAgentFromDeploymentState(state model.DeploymentState, targetName string) string { for _, targetDes := range state.Targets { if targetName == targetDes.Name { diff --git a/api/pkg/apis/v1alpha1/model/target.go b/api/pkg/apis/v1alpha1/model/target.go index 7c3603534..f14abe595 100644 --- a/api/pkg/apis/v1alpha1/model/target.go +++ b/api/pkg/apis/v1alpha1/model/target.go @@ -24,6 +24,7 @@ type ( TargetSpec struct { DisplayName string `json:"displayName,omitempty"` Scope string `json:"scope,omitempty"` + SolutionScope string `json:"solutionScope,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` Properties map[string]string `json:"properties,omitempty"` Components []ComponentSpec `json:"components,omitempty"` @@ -48,6 +49,10 @@ func (c TargetSpec) DeepEquals(other IDeepEquals) (bool, error) { return false, nil } + if c.SolutionScope != otherC.SolutionScope { + return false, nil + } + if !StringMapsEqual(c.Metadata, otherC.Metadata, nil) { return false, nil } diff --git a/api/pkg/apis/v1alpha1/utils/symphony-api.go b/api/pkg/apis/v1alpha1/utils/symphony-api.go index 484bddd48..50bccc019 100644 --- a/api/pkg/apis/v1alpha1/utils/symphony-api.go +++ b/api/pkg/apis/v1alpha1/utils/symphony-api.go @@ -752,10 +752,6 @@ func CreateSymphonyDeployment(ctx context.Context, instance model.InstanceState, sTargets[t.ObjectMeta.Name] = t } - if instance.Spec.Scope == "" { - instance.Spec.Scope = constants.DefaultScope - } - //TODO: handle devices ret.Solution = solution ret.Targets = sTargets diff --git a/api/pkg/apis/v1alpha1/utils/symphony-api_test.go b/api/pkg/apis/v1alpha1/utils/symphony-api_test.go index 667d69a18..3251d2829 100644 --- a/api/pkg/apis/v1alpha1/utils/symphony-api_test.go +++ b/api/pkg/apis/v1alpha1/utils/symphony-api_test.go @@ -702,7 +702,6 @@ func TestCreateSymphonyDeployment(t *testing.T) { }, Spec: &model.InstanceSpec{ Solution: "", - Scope: "default", // CreateSymphonyDeployment will give default if instance.Spec.Scope is empty Target: model.TargetSelector{ Name: "someTargetName", Selector: map[string]string{ diff --git a/k8s/apis/model/v1/common_types.go b/k8s/apis/model/v1/common_types.go index 560c1da0e..cbf8b24bd 100644 --- a/k8s/apis/model/v1/common_types.go +++ b/k8s/apis/model/v1/common_types.go @@ -79,6 +79,7 @@ type TargetSpec struct { DisplayName string `json:"displayName,omitempty"` Metadata map[string]string `json:"metadata,omitempty"` Scope string `json:"scope,omitempty"` + SolutionScope string `json:"solutionScope,omitempty"` Properties map[string]string `json:"properties,omitempty"` Components []ComponentSpec `json:"components,omitempty"` Constraints string `json:"constraints,omitempty"` diff --git a/k8s/config/oss/crd/bases/fabric.symphony_targets.yaml b/k8s/config/oss/crd/bases/fabric.symphony_targets.yaml index 6cd843342..1487a5bce 100644 --- a/k8s/config/oss/crd/bases/fabric.symphony_targets.yaml +++ b/k8s/config/oss/crd/bases/fabric.symphony_targets.yaml @@ -43,6 +43,8 @@ spec: spec: description: Defines the desired state of Target properties: + solutionScope: + type: string components: items: description: Defines a desired runtime component diff --git a/k8s/utils/symphony-api.go b/k8s/utils/symphony-api.go index 2f037d21c..78e245365 100644 --- a/k8s/utils/symphony-api.go +++ b/k8s/utils/symphony-api.go @@ -85,6 +85,7 @@ func K8STargetToAPITargetState(target fabric_v1.Target) (apimodel.TargetState, e DisplayName: target.Spec.DisplayName, Metadata: target.Spec.Metadata, Scope: target.Spec.Scope, + SolutionScope: target.Spec.SolutionScope, Properties: target.Spec.Properties, Constraints: target.Spec.Constraints, ForceRedeploy: target.Spec.ForceRedeploy, diff --git a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml index 237cd2db0..da0ffaf8f 100644 --- a/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml +++ b/packages/helm/symphony/templates/symphony-core/symphonyk8s.yaml @@ -1684,6 +1684,8 @@ spec: spec: description: Defines the desired state of Target properties: + solutionScope: + type: string components: items: description: Defines a desired runtime component diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-configmap-default.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-configmap-default.yaml new file mode 100644 index 000000000..e38548c42 --- /dev/null +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-configmap-default.yaml @@ -0,0 +1,8 @@ +apiVersion: solution.symphony/v1 +kind: Instance +metadata: + name: instance-configmap +spec: + solution: solution-configmap:v1 + target: + name: target-configmap diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-configmap-with-scope.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-configmap-with-scope.yaml new file mode 100644 index 000000000..6891343b6 --- /dev/null +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/instance-configmap-with-scope.yaml @@ -0,0 +1,9 @@ +apiVersion: solution.symphony/v1 +kind: Instance +metadata: + name: instance-configmap +spec: + scope: nondefault + solution: solution-configmap:v1 + target: + name: target-configmap diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution-configmap.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution-configmap.yaml new file mode 100644 index 000000000..bccd683f1 --- /dev/null +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/solution-configmap.yaml @@ -0,0 +1,17 @@ +apiVersion: solution.symphony/v1 +kind: SolutionContainer +metadata: + name: solution-configmap +spec: +--- +apiVersion: solution.symphony/v1 +kind: Solution +metadata: + name: solution-configmap-v-v1 +spec: + rootResource: solution-configmap + components: + - name: configmap + type: config + properties: + tags: "test-tag" \ No newline at end of file diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-configmap-default.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-configmap-default.yaml new file mode 100644 index 000000000..e4f64c260 --- /dev/null +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-configmap-default.yaml @@ -0,0 +1,11 @@ +apiVersion: fabric.symphony/v1 +kind: Target +metadata: + name: target-configmap +spec: + topologies: + - bindings: + - role: config + provider: providers.target.configmap + config: + inCluster: "true" \ No newline at end of file diff --git a/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-configmap.yaml b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-configmap.yaml new file mode 100644 index 000000000..387c179c0 --- /dev/null +++ b/test/integration/scenarios/03.basicWithNsDelete/manifest/oss/target-configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: fabric.symphony/v1 +kind: Target +metadata: + name: target-configmap +spec: + solutionScope: target-scope + topologies: + - bindings: + - role: config + provider: providers.target.configmap + config: + inCluster: "true" \ No newline at end of file diff --git a/test/integration/scenarios/03.basicWithNsDelete/verify/manifest_test.go b/test/integration/scenarios/03.basicWithNsDelete/verify/manifest_test.go index 56a4fb2b5..68e4ca622 100644 --- a/test/integration/scenarios/03.basicWithNsDelete/verify/manifest_test.go +++ b/test/integration/scenarios/03.basicWithNsDelete/verify/manifest_test.go @@ -415,6 +415,86 @@ func TestBasic_VerifySameInstanceRecreationInNamespace(t *testing.T) { } } +func TestBasic_VerifyTargetSolutionScope(t *testing.T) { + // Manifests to deploy + var testManifests = []string{ + "../manifest/oss/solution-configmap.yaml", + "../manifest/oss/target-configmap-default.yaml", + "../manifest/oss/instance-configmap-default.yaml", + } + + // Deploy the manifests in default namespace + for _, manifest := range testManifests { + fullPath, err := filepath.Abs(manifest) + require.NoError(t, err) + + err = shellcmd.Command(fmt.Sprintf("kubectl apply -f %s -n default", fullPath)).Run() + require.NoError(t, err) + } + + cfg, err := testhelpers.RestConfig() + require.NoError(t, err) + clientset, err := kubernetes.NewForConfig(cfg) + require.NoError(t, err) + + // Verify configmap in default scope + for { + namespace := "default" + configMapName := "configmap" + configMap, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.Background(), configMapName, metav1.GetOptions{}) + if err == nil { + require.Equal(t, "test-tag", configMap.Data["tags"], "configmap data should match the input") + break + } + + sleepDuration, _ := time.ParseDuration("5s") + time.Sleep(sleepDuration) + } + + // update target with solutionScope + targetFile := "../manifest/oss/target-configmap.yaml" + fullPath, err := filepath.Abs(targetFile) + require.NoError(t, err) + err = shellcmd.Command(fmt.Sprintf("kubectl apply -f %s -n default", fullPath)).Run() + require.NoError(t, err) + + // Verify configmp in target solutionScope + for { + namespace := "target-scope" + configMapName := "configmap" + configMap, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.Background(), configMapName, metav1.GetOptions{}) + if err == nil { + require.Equal(t, "test-tag", configMap.Data["tags"], "configmap data should match the input") + break + } + + sleepDuration, _ := time.ParseDuration("5s") + time.Sleep(sleepDuration) + } + + // Update instance scope with nondefault namespace + instanceFile := "../manifest/oss/instance-configmap-with-scope.yaml" + fullPath, err = filepath.Abs(instanceFile) + require.NoError(t, err) + err = shellcmd.Command(fmt.Sprintf("kubectl apply -f %s -n default", fullPath)).Run() + require.NoError(t, err) + + // Verify configmap in nondefault instance scope + for { + namespace := "nondefault" + configMapName := "configmap" + configMap, err := clientset.CoreV1().ConfigMaps(namespace).Get(context.Background(), configMapName, metav1.GetOptions{}) + if err == nil { + require.Equal(t, "test-tag", configMap.Data["tags"], "configmap data should match the input") + break + } + + sleepDuration, _ := time.ParseDuration("5s") + time.Sleep(sleepDuration) + } + +} + // Helper for read catalog func readCatalog(catalogName string, namespace string, dynamicClient dynamic.Interface) (*unstructured.Unstructured, error) { gvr := schema.GroupVersionResource{Group: "federation.symphony", Version: "v1", Resource: "catalogs"}