From 476ea144ffd7d212becd1c1a5248e94355c181ab Mon Sep 17 00:00:00 2001 From: "youngtae88.kim" Date: Wed, 2 Oct 2024 12:04:26 +0900 Subject: [PATCH 1/3] init piccolo --- api/go.mod | 2 + .../v1alpha1/providers/providerfactory.go | 15 + .../providers/providerfactory_test.go | 14 + .../providers/target/piccolo/piccolo.go | 221 +++++++++++ .../providers/target/piccolo/piccolo_test.go | 364 ++++++++++++++++++ 5 files changed, 616 insertions(+) create mode 100644 api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go create mode 100644 api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo_test.go diff --git a/api/go.mod b/api/go.mod index 1c1f117d0..ea901d09c 100644 --- a/api/go.mod +++ b/api/go.mod @@ -8,6 +8,8 @@ replace github.com/eclipse-symphony/symphony/coa => ../coa replace github.com/eclipse-symphony/symphony/packages/mage => ../packages/mage +replace "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/piccolo" => ./pkg/apis/v1alpha1/providers/target/piccolo + require ( github.com/eclipse-symphony/symphony/coa v0.0.0 github.com/spf13/cobra v1.8.1 diff --git a/api/pkg/apis/v1alpha1/providers/providerfactory.go b/api/pkg/apis/v1alpha1/providers/providerfactory.go index f238729ce..26691c27a 100644 --- a/api/pkg/apis/v1alpha1/providers/providerfactory.go +++ b/api/pkg/apis/v1alpha1/providers/providerfactory.go @@ -37,6 +37,7 @@ import ( "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/kubectl" tgtmock "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/mock" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/mqtt" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/piccolo" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/proxy" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/script" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/staging" @@ -195,6 +196,12 @@ func (s SymphonyProviderFactory) CreateProvider(providerType string, config cp.I if err == nil { return mProvider, nil } + case "providers.target.piccolo": + mProvider := &piccolo.PiccoloTargetProvider{} + err = mProvider.Init(config) + if err == nil { + return mProvider, nil + } case "providers.target.ingress": mProvider := &ingress.IngressTargetProvider{} err = mProvider.Init(config) @@ -414,6 +421,14 @@ func CreateProviderForTargetRole(context *contexts.ManagerContext, role string, } provider.Context = context return provider, nil + case "providers.target.piccolo": + provider := &piccolo.PiccoloTargetProvider{} + err := provider.InitWithMap(binding.Config) + if err != nil { + return nil, err + } + provider.Context = context + return provider, nil case "providers.target.ingress": provider := &ingress.IngressTargetProvider{} err := provider.InitWithMap(binding.Config) diff --git a/api/pkg/apis/v1alpha1/providers/providerfactory_test.go b/api/pkg/apis/v1alpha1/providers/providerfactory_test.go index bf68a7268..44cd27d73 100644 --- a/api/pkg/apis/v1alpha1/providers/providerfactory_test.go +++ b/api/pkg/apis/v1alpha1/providers/providerfactory_test.go @@ -35,6 +35,7 @@ import ( "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/k8s" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/kubectl" tgtmock "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/mock" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/piccolo" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/proxy" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/script" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/staging" @@ -158,6 +159,10 @@ func TestCreateProvider(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*docker.DockerTargetProvider)) + provider, err = providerfactory.CreateProvider("providers.target.piccolo", piccolo.PiccoloTargetProviderConfig{}) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*piccolo.PiccoloTargetProvider)) + if getTestMiniKubeEnabled == "" { t.Log("Skipping providers.target.ingress test as TEST_MINIKUBE_ENABLED is not set") } else { @@ -348,6 +353,11 @@ func TestCreateProviderForTargetRole(t *testing.T) { Provider: "providers.target.docker", Config: map[string]string{}, }, + { + Role: "piccolo", + Provider: "providers.target.piccolo", + Config: map[string]string{}, + }, { Role: "ingress", Provider: "providers.target.ingress", @@ -634,6 +644,10 @@ func TestCreateProviderForTargetRole(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, *provider.(*docker.DockerTargetProvider)) + provider, err = CreateProviderForTargetRole(nil, "piccolo", targetState, nil) + assert.Nil(t, err) + assert.NotNil(t, *provider.(*piccolo.PiccoloTargetProvider)) + if getTestMiniKubeEnabled == "" { t.Log("Skipping ingress test as TEST_MINIKUBE_ENABLED is not set") } else { diff --git a/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go new file mode 100644 index 000000000..cf6a78f18 --- /dev/null +++ b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go @@ -0,0 +1,221 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package piccolo + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "net/http" + + "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/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/logger" +) + +const loggerName = "providers.target.piccolo" + +var sLog = logger.NewLogger(loggerName) + +type PiccoloTargetProviderConfig struct { + Name string `json:"name"` + Url string `json:"url"` +} + +type PiccoloTargetProvider struct { + Config PiccoloTargetProviderConfig + Context *contexts.ManagerContext +} + +func PiccoloTargetProviderConfigFromMap(properties map[string]string) (PiccoloTargetProviderConfig, error) { + ret := PiccoloTargetProviderConfig{} + if v, ok := properties["name"]; ok { + ret.Name = v + } + return ret, nil +} +func (d *PiccoloTargetProvider) InitWithMap(properties map[string]string) error { + config, err := PiccoloTargetProviderConfigFromMap(properties) + if err != nil { + return err + } + return d.Init(config) +} +func (s *PiccoloTargetProvider) SetContext(ctx *contexts.ManagerContext) { + s.Context = ctx +} + +func (d *PiccoloTargetProvider) Init(config providers.IProviderConfig) error { + _, span := observability.StartSpan("Piccolo Target Provider", context.TODO(), &map[string]string{ + "method": "Init", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + sLog.Info(" P (Piccolo Target): Init()") + + // convert config to PiccoloTargetProviderConfig type + piccoloConfig, err := toPiccoloTargetProviderConfig(config) + if err != nil { + sLog.Errorf(" P (Piccolo Target): expected PiccoloTargetProviderConfig: %+v", err) + return err + } + + d.Config = piccoloConfig + return nil +} + +func toPiccoloTargetProviderConfig(config providers.IProviderConfig) (PiccoloTargetProviderConfig, error) { + ret := PiccoloTargetProviderConfig{} + data, err := json.Marshal(config) + if err != nil { + return ret, err + } + err = json.Unmarshal(data, &ret) + return ret, err +} + +func (i *PiccoloTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) { + ctx, span := observability.StartSpan("Piccolo Target Provider", ctx, &map[string]string{ + "method": "Get", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + sLog.InfofCtx(ctx, " P (Piccolo Target): getting artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) + + ret := make([]model.ComponentSpec, 0) + for _, component := range references { + properties := component.Component.Properties + name := properties["workload.name"].(string) + workloadType := properties["workload.type"].(string) + req, err := http.NewRequest("GET", i.Config.Url+workloadType+"/"+name, nil) + if err != nil { + sLog.ErrorCtx(ctx, " P (Piccolo Target): Unable to make Request") + return nil, err + } + client := &http.Client{} + resp, err := client.Do(req) + + if err != nil { + sLog.ErrorfCtx(ctx, " P (Piccolo Target): Unable to get workload %s from piccolo", name) + return nil, err + } + + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + // respBody, err := io.ReadAll(resp.Body) + component := model.ComponentSpec{ + Name: name, + Properties: make(map[string]interface{}), + } + component.Properties["workload.name"] = string(name) + component.Properties["workload.type"] = string(workloadType) + ret = append(ret, component) + case http.StatusNotFound: + continue + } + } + + return ret, nil +} + +func (i *PiccoloTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) { + ctx, span := observability.StartSpan("Piccolo Target Provider", ctx, &map[string]string{ + "method": "Apply", + }) + var err error = nil + defer observ_utils.CloseSpanWithError(span, &err) + + sLog.InfofCtx(ctx, " P (Piccolo Target): applying artifacts: %s - %s, traceId: %s", deployment.Instance.Spec.Scope, deployment.Instance.ObjectMeta.Name, span.SpanContext().TraceID().String()) + + injections := &model.ValueInjections{ + InstanceId: deployment.Instance.ObjectMeta.Name, + SolutionId: deployment.Instance.Spec.Solution, + TargetId: deployment.ActiveTarget, + } + + components := step.GetComponents() + err = i.GetValidationRule(ctx).Validate(components) + if err != nil { + sLog.ErrorfCtx(ctx, " P (Piccolo Target): failed to validate components: %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + return nil, err + } + if isDryRun { + err = nil + return nil, nil + } + + ret := step.PrepareResultMap() + + for _, component := range step.Components { + if component.Action == model.ComponentUpdate { + name := model.ReadPropertyCompat(component.Component.Properties, "workload.name", injections) + if name == "" { + err = errors.New("component doesn't have workload.name property") + ret[component.Component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.UpdateFailed, + Message: err.Error(), + } + sLog.ErrorfCtx(ctx, " P (Piccolo Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + return ret, err + } + reqBody := bytes.NewBufferString("https:// scenario path") + resp, err := http.Post(i.Config.Url+"create-scenario/"+component.Component.Name, "text/plain", reqBody) + if err != nil { + sLog.ErrorCtx(ctx, " P (Piccolo Target): fail to create resource") + return ret, err + } + + defer resp.Body.Close() + + ret[component.Component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.Updated, + Message: "", + } + } else { + req, err := http.NewRequest("DELETE", i.Config.Url+"delete-scenario/"+component.Component.Name, nil) + if err != nil { + return ret, err + } + + client := &http.Client{} + _, err = client.Do(req) + + if err == nil { + ret[component.Component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.Deleted, + Message: "", + } + } + } + } + return ret, nil +} + +func (*PiccoloTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule { + return model.ValidationRule{ + AllowSidecar: false, + ComponentValidationRule: model.ComponentValidationRule{ + RequiredProperties: []string{"workload.type", "workload.name"}, + OptionalProperties: []string{}, + RequiredComponentType: "", + RequiredMetadata: []string{}, + OptionalMetadata: []string{}, + ChangeDetectionProperties: []model.PropertyDesc{ + {Name: "workload.name", IgnoreCase: false, SkipIfMissing: false}, + }, + }, + } +} diff --git a/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo_test.go b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo_test.go new file mode 100644 index 000000000..7da0997a6 --- /dev/null +++ b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo_test.go @@ -0,0 +1,364 @@ +/* + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * SPDX-License-Identifier: MIT + */ + +package piccolo + +import ( + "context" + "os" + "testing" + + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" + "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/conformance" + "github.com/stretchr/testify/assert" +) + +func TestPiccoloTargetProviderConfigFromMapNil(t *testing.T) { + _, err := PiccoloTargetProviderConfigFromMap(nil) + assert.Nil(t, err) +} +func TestPiccoloTargetProviderConfigFromMapEmpty(t *testing.T) { + _, err := PiccoloTargetProviderConfigFromMap(map[string]string{}) + assert.Nil(t, err) +} +func TestPiccoloTargetProviderInitEmptyConfig(t *testing.T) { + config := PiccoloTargetProviderConfig{} + provider := PiccoloTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) +} +func TestInitWithMap(t *testing.T) { + configMap := map[string]string{ + "name": "name", + } + provider := PiccoloTargetProvider{} + err := provider.InitWithMap(configMap) + assert.Nil(t, err) +} +func TestPiccoloTargetProviderInstall(t *testing.T) { + testPiccoloProvider := os.Getenv("TEST_PICCOLO_PROVIDER") + if testPiccoloProvider == "" { + t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + } + config := PiccoloTargetProviderConfig{} + provider := PiccoloTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + component := model.ComponentSpec{ + Name: "redis-test", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "redis:latest", + }, + } + deployment := model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{}, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + } + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) +} + +func TestPiccoloTargetProviderGet(t *testing.T) { + // NOTE: To run this test case successfully, you need to have Docker and Redis container running: + // docker run -d --name redis-test -p 6379:6379 redis:latest + // Then, comment out the next 4 lines of code and run the test case. + testPiccoloProvider := os.Getenv("TEST_PICCOLO_PROVIDER") + if testPiccoloProvider == "" { + t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + } + config := PiccoloTargetProviderConfig{} + provider := PiccoloTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + components, err := provider.Get(context.Background(), model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{}, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{ + { + Name: "redis-test", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "redis:latest", + }, + }, + }, + }, + }, + }, []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: model.ComponentSpec{ + Name: "redis-test", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "redis:latest", + "env.REDIS_VERSION": "7.0.12", // NOTE: Only environment variables passed in by the reference are returned. + }, + }, + }, + }) + assert.Nil(t, err) + assert.Equal(t, 1, len(components)) + assert.Equal(t, "redis:latest", components[0].Properties["workload.name"]) + assert.NotEqual(t, "", components[0].Properties["env.REDIS_VERSION"]) +} +func TestPiccoloTargetProviderRemove(t *testing.T) { + testPiccoloProvider := os.Getenv("TEST_PICCOLO_PROVIDER") + if testPiccoloProvider == "" { + t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + } + config := PiccoloTargetProviderConfig{} + provider := PiccoloTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + component := model.ComponentSpec{ + Name: "redis-test", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "redis:latest", + }, + } + deployment := model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{}, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + } + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentDelete, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) +} + +func TestUpdateGetDelete(t *testing.T) { + testPiccoloProvider := os.Getenv("TEST_PICCOLO_ENABLED") + if testPiccoloProvider == "" { + t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + } + config := PiccoloTargetProviderConfig{} + provider := PiccoloTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + + // Update + component := model.ComponentSpec{ + Name: "alpine-test", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "alpine:3.18", + }, + } + deployment := model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{}, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + } + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) + + // Get + components, err := provider.Get(context.Background(), model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{}, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{ + { + Name: "alpine-test", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "alpine:3.18", + }, + }, + }, + }, + }, + }, []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: model.ComponentSpec{ + Name: "alpine-test", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "alpine:3.18", + }, + }, + }, + }) + assert.Nil(t, err) + assert.Equal(t, 1, len(components)) + + // Delete + step = model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentDelete, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) +} + +func TestApplyFailed(t *testing.T) { + testPiccoloProvider := os.Getenv("TEST_PICCOLO_ENABLED") + if testPiccoloProvider == "" { + t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + } + config := PiccoloTargetProviderConfig{} + provider := PiccoloTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + + // invalid container image name + component := model.ComponentSpec{ + Name: "", + Type: "container", + } + deployment := model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{}, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + } + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, + }, + } + + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.NotNil(t, err) + + // unknown container image + component = model.ComponentSpec{ + Name: "abcd:latest", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "abc:latest", + }, + } + deployment = model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{}, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + } + step = model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.NotNil(t, err) +} + +func TestApplyAlreadyRunning(t *testing.T) { + testPiccoloProvider := os.Getenv("TEST_PICCOLO_ENABLED") + if testPiccoloProvider == "" { + t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + } + config := PiccoloTargetProviderConfig{} + provider := PiccoloTargetProvider{} + err := provider.Init(config) + assert.Nil(t, err) + + component := model.ComponentSpec{ + Name: "alpine-test", + Type: "container", + Properties: map[string]interface{}{ + "workload.name": "alpine:3.18", + }, + } + deployment := model.DeploymentSpec{ + Instance: model.InstanceState{ + Spec: &model.InstanceSpec{}, + }, + Solution: model.SolutionState{ + Spec: &model.SolutionSpec{ + Components: []model.ComponentSpec{component}, + }, + }, + } + step := model.DeploymentStep{ + Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, + }, + } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) + + // already running + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.Nil(t, err) +} + +func TestConformanceSuite(t *testing.T) { + provider := &PiccoloTargetProvider{} + err := provider.Init(PiccoloTargetProviderConfig{}) + assert.Nil(t, err) + conformance.ConformanceSuite(t, provider) +} From 34ae5d56c7663d208f49ba3b0697c4b73191cac8 Mon Sep 17 00:00:00 2001 From: "youngtae88.kim" Date: Fri, 4 Oct 2024 09:41:13 +0900 Subject: [PATCH 2/3] init piccolo - delete workload.type - add Piccolo URL setting - use workload.name instead of component name during Apply --- .../v1alpha1/providers/target/piccolo/piccolo.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go index cf6a78f18..0618667e8 100644 --- a/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go +++ b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go @@ -23,6 +23,7 @@ import ( ) const loggerName = "providers.target.piccolo" +const defaultPiccoloApiServer = "http://0.0.0.0:9090/" var sLog = logger.NewLogger(loggerName) @@ -41,6 +42,11 @@ func PiccoloTargetProviderConfigFromMap(properties map[string]string) (PiccoloTa if v, ok := properties["name"]; ok { ret.Name = v } + if v, ok := properties["url"]; ok { + ret.Url = v + } else { + ret.Url = defaultPiccoloApiServer + } return ret, nil } func (d *PiccoloTargetProvider) InitWithMap(properties map[string]string) error { @@ -97,8 +103,8 @@ func (i *PiccoloTargetProvider) Get(ctx context.Context, deployment model.Deploy for _, component := range references { properties := component.Component.Properties name := properties["workload.name"].(string) - workloadType := properties["workload.type"].(string) - req, err := http.NewRequest("GET", i.Config.Url+workloadType+"/"+name, nil) + + req, err := http.NewRequest("GET", i.Config.Url+"scenario/"+name, nil) if err != nil { sLog.ErrorCtx(ctx, " P (Piccolo Target): Unable to make Request") return nil, err @@ -121,7 +127,6 @@ func (i *PiccoloTargetProvider) Get(ctx context.Context, deployment model.Deploy Properties: make(map[string]interface{}), } component.Properties["workload.name"] = string(name) - component.Properties["workload.type"] = string(workloadType) ret = append(ret, component) case http.StatusNotFound: continue @@ -172,7 +177,7 @@ func (i *PiccoloTargetProvider) Apply(ctx context.Context, deployment model.Depl return ret, err } reqBody := bytes.NewBufferString("https:// scenario path") - resp, err := http.Post(i.Config.Url+"create-scenario/"+component.Component.Name, "text/plain", reqBody) + resp, err := http.Post(i.Config.Url+"create-scenario/"+name, "text/plain", reqBody) if err != nil { sLog.ErrorCtx(ctx, " P (Piccolo Target): fail to create resource") return ret, err @@ -208,7 +213,7 @@ func (*PiccoloTargetProvider) GetValidationRule(ctx context.Context) model.Valid return model.ValidationRule{ AllowSidecar: false, ComponentValidationRule: model.ComponentValidationRule{ - RequiredProperties: []string{"workload.type", "workload.name"}, + RequiredProperties: []string{"workload.name"}, OptionalProperties: []string{}, RequiredComponentType: "", RequiredMetadata: []string{}, From b1fdcfd2322c18e312d37f113e6b774ceaa88fc3 Mon Sep 17 00:00:00 2001 From: "youngtae88.kim" Date: Thu, 12 Dec 2024 10:59:41 +0900 Subject: [PATCH 3/3] update piccolo, test and mock server --- .../providers/target/piccolo/piccolo.go | 33 ++- .../providers/target/piccolo/piccolo_test.go | 266 ++++++++---------- docs/mocks/piccolo-mock/app.py | 44 ++- 3 files changed, 166 insertions(+), 177 deletions(-) diff --git a/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go index 0618667e8..a8d5b3ea3 100644 --- a/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go +++ b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo.go @@ -23,7 +23,7 @@ import ( ) const loggerName = "providers.target.piccolo" -const defaultPiccoloApiServer = "http://0.0.0.0:9090/" +const defaultPiccoloApiServer = "http://0.0.0.0:47099" var sLog = logger.NewLogger(loggerName) @@ -104,7 +104,7 @@ func (i *PiccoloTargetProvider) Get(ctx context.Context, deployment model.Deploy properties := component.Component.Properties name := properties["workload.name"].(string) - req, err := http.NewRequest("GET", i.Config.Url+"scenario/"+name, nil) + req, err := http.NewRequest("GET", i.Config.Url+"/scenario/"+name, nil) if err != nil { sLog.ErrorCtx(ctx, " P (Piccolo Target): Unable to make Request") return nil, err @@ -129,7 +129,8 @@ func (i *PiccoloTargetProvider) Get(ctx context.Context, deployment model.Deploy component.Properties["workload.name"] = string(name) ret = append(ret, component) case http.StatusNotFound: - continue + err = errors.New(" P (Piccolo Target): Unable to get workload " + name + " from piccolo") + return nil, err } } @@ -165,8 +166,8 @@ func (i *PiccoloTargetProvider) Apply(ctx context.Context, deployment model.Depl ret := step.PrepareResultMap() for _, component := range step.Components { + name := model.ReadPropertyCompat(component.Component.Properties, "workload.name", injections) if component.Action == model.ComponentUpdate { - name := model.ReadPropertyCompat(component.Component.Properties, "workload.name", injections) if name == "" { err = errors.New("component doesn't have workload.name property") ret[component.Component.Name] = model.ComponentResultSpec{ @@ -176,9 +177,9 @@ func (i *PiccoloTargetProvider) Apply(ctx context.Context, deployment model.Depl sLog.ErrorfCtx(ctx, " P (Piccolo Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) return ret, err } - reqBody := bytes.NewBufferString("https:// scenario path") - resp, err := http.Post(i.Config.Url+"create-scenario/"+name, "text/plain", reqBody) - if err != nil { + reqBody := bytes.NewBufferString(name) + resp, err := http.Post(i.Config.Url+"/scenario", "text/plain", reqBody) + if err != nil || resp.StatusCode != http.StatusCreated { sLog.ErrorCtx(ctx, " P (Piccolo Target): fail to create resource") return ret, err } @@ -190,19 +191,31 @@ func (i *PiccoloTargetProvider) Apply(ctx context.Context, deployment model.Depl Message: "", } } else { - req, err := http.NewRequest("DELETE", i.Config.Url+"delete-scenario/"+component.Component.Name, nil) + if name == "" { + err = errors.New("component doesn't have workload.name property") + ret[component.Component.Name] = model.ComponentResultSpec{ + Status: v1alpha2.DeleteFailed, + Message: err.Error(), + } + sLog.ErrorfCtx(ctx, " P (Piccolo Target): %+v, traceId: %s", err, span.SpanContext().TraceID().String()) + return ret, err + } + req, err := http.NewRequest("DELETE", i.Config.Url+"/scenario/"+name, nil) if err != nil { return ret, err } client := &http.Client{} - _, err = client.Do(req) + resp, err := client.Do(req) - if err == nil { + if err == nil && resp.StatusCode == http.StatusOK { ret[component.Component.Name] = model.ComponentResultSpec{ Status: v1alpha2.Deleted, Message: "", } + } else { + err = errors.New(" P (Piccolo Target): Unable to delete workload " + name + " from piccolo") + return nil, err } } } diff --git a/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo_test.go b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo_test.go index 7da0997a6..6ab6d4b3c 100644 --- a/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo_test.go +++ b/api/pkg/apis/v1alpha1/providers/target/piccolo/piccolo_test.go @@ -8,7 +8,7 @@ package piccolo import ( "context" - "os" + "os/exec" "testing" "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model" @@ -38,20 +38,31 @@ func TestInitWithMap(t *testing.T) { err := provider.InitWithMap(configMap) assert.Nil(t, err) } -func TestPiccoloTargetProviderInstall(t *testing.T) { - testPiccoloProvider := os.Getenv("TEST_PICCOLO_PROVIDER") - if testPiccoloProvider == "" { - t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") +func TestPiccoloTargetProviderApply(t *testing.T) { + // NOTE: To run this test case successfully, you need to have Docker and Redis container running: + // docker run -d -p 5000:5000 --name mock_piccolo hbai/piccolo-mock:latest + cmd := exec.Command("podman", "ps", "-q", "-f", "name=mock_piccolo") + output, err := cmd.Output() + assert.Nil(t, err) + if len(output) == 0 { + t.Skip("Skipping because mock_picc container is not exist") } - config := PiccoloTargetProviderConfig{} + + configMap := map[string]string{ + "name": "piccolo", + "url": "http://127.0.0.1:5000/", + } + config, err := PiccoloTargetProviderConfigFromMap(configMap) + assert.Nil(t, err) + provider := PiccoloTargetProvider{} - err := provider.Init(config) + err = provider.Init(config) assert.Nil(t, err) component := model.ComponentSpec{ - Name: "redis-test", + Name: "redis", Type: "container", Properties: map[string]interface{}{ - "workload.name": "redis:latest", + "workload.name": "redis-test", }, } deployment := model.DeploymentSpec{ @@ -78,15 +89,24 @@ func TestPiccoloTargetProviderInstall(t *testing.T) { func TestPiccoloTargetProviderGet(t *testing.T) { // NOTE: To run this test case successfully, you need to have Docker and Redis container running: - // docker run -d --name redis-test -p 6379:6379 redis:latest - // Then, comment out the next 4 lines of code and run the test case. - testPiccoloProvider := os.Getenv("TEST_PICCOLO_PROVIDER") - if testPiccoloProvider == "" { - t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + // docker run -d -p 5000:5000 --name mock_piccolo hbai/piccolo-mock:latest + // Befor this, TestPiccoloTargetProviderApply must be called. + TestPiccoloTargetProviderApply(t) + cmd := exec.Command("podman", "ps", "-q", "-f", "name=mock_piccolo") + output, err := cmd.Output() + assert.Nil(t, err) + if len(output) == 0 { + t.Skip("Skipping because mock_picc container is not exist") } - config := PiccoloTargetProviderConfig{} + + configMap := map[string]string{ + "name": "piccolo", + "url": "http://127.0.0.1:5000", + } + config, err := PiccoloTargetProviderConfigFromMap(configMap) + assert.Nil(t, err) provider := PiccoloTargetProvider{} - err := provider.Init(config) + err = provider.Init(config) assert.Nil(t, err) components, err := provider.Get(context.Background(), model.DeploymentSpec{ Instance: model.InstanceState{ @@ -96,10 +116,10 @@ func TestPiccoloTargetProviderGet(t *testing.T) { Spec: &model.SolutionSpec{ Components: []model.ComponentSpec{ { - Name: "redis-test", + Name: "redis", Type: "container", Properties: map[string]interface{}{ - "workload.name": "redis:latest", + "workload.name": "redis-test", }, }, }, @@ -109,34 +129,42 @@ func TestPiccoloTargetProviderGet(t *testing.T) { { Action: model.ComponentUpdate, Component: model.ComponentSpec{ - Name: "redis-test", + Name: "redis", Type: "container", Properties: map[string]interface{}{ - "workload.name": "redis:latest", - "env.REDIS_VERSION": "7.0.12", // NOTE: Only environment variables passed in by the reference are returned. + "workload.name": "redis-test", }, }, }, }) assert.Nil(t, err) assert.Equal(t, 1, len(components)) - assert.Equal(t, "redis:latest", components[0].Properties["workload.name"]) - assert.NotEqual(t, "", components[0].Properties["env.REDIS_VERSION"]) + assert.Equal(t, "redis-test", components[0].Properties["workload.name"]) } -func TestPiccoloTargetProviderRemove(t *testing.T) { - testPiccoloProvider := os.Getenv("TEST_PICCOLO_PROVIDER") - if testPiccoloProvider == "" { - t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") +func TestPiccoloTargetProviderApplyDelete(t *testing.T) { + // NOTE: To run this test case successfully, you need to have Docker and Redis container running: + // docker run -d -p 5000:5000 --name mock_piccolo hbai/piccolo-mock:latest + cmd := exec.Command("podman", "ps", "-q", "-f", "name=mock_piccolo") + output, err := cmd.Output() + assert.Nil(t, err) + if len(output) == 0 { + t.Skip("Skipping because mock_picc container is not exist") } - config := PiccoloTargetProviderConfig{} + + configMap := map[string]string{ + "name": "piccolo", + "url": "http://127.0.0.1:5000", + } + config, err := PiccoloTargetProviderConfigFromMap(configMap) + assert.Nil(t, err) provider := PiccoloTargetProvider{} - err := provider.Init(config) + err = provider.Init(config) assert.Nil(t, err) component := model.ComponentSpec{ - Name: "redis-test", + Name: "redis", Type: "container", Properties: map[string]interface{}{ - "workload.name": "redis:latest", + "workload.name": "redis-test", }, } deployment := model.DeploymentSpec{ @@ -151,6 +179,10 @@ func TestPiccoloTargetProviderRemove(t *testing.T) { } step := model.DeploymentStep{ Components: []model.ComponentStep{ + { + Action: model.ComponentUpdate, + Component: component, + }, { Action: model.ComponentDelete, Component: component, @@ -160,24 +192,30 @@ func TestPiccoloTargetProviderRemove(t *testing.T) { _, err = provider.Apply(context.Background(), deployment, step, false) assert.Nil(t, err) } +func TestApplyFailed(t *testing.T) { + // NOTE: To run this test case successfully, you need to have Docker and Redis container running: + // docker run -d -p 5000:5000 --name mock_piccolo hbai/piccolo-mock:latest + cmd := exec.Command("podman", "ps", "-q", "-f", "name=mock_piccolo") + output, err := cmd.Output() + assert.Nil(t, err) + if len(output) == 0 { + t.Skip("Skipping because mock_picc container is not exist") + } -func TestUpdateGetDelete(t *testing.T) { - testPiccoloProvider := os.Getenv("TEST_PICCOLO_ENABLED") - if testPiccoloProvider == "" { - t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + configMap := map[string]string{ + "name": "piccolo", + "url": "http://127.0.0.1:5000", } - config := PiccoloTargetProviderConfig{} + config, err := PiccoloTargetProviderConfigFromMap(configMap) + assert.Nil(t, err) provider := PiccoloTargetProvider{} - err := provider.Init(config) + err = provider.Init(config) assert.Nil(t, err) - // Update + // invalid component properties component := model.ComponentSpec{ - Name: "alpine-test", + Name: "", Type: "container", - Properties: map[string]interface{}{ - "workload.name": "alpine:3.18", - }, } deployment := model.DeploymentSpec{ Instance: model.InstanceState{ @@ -197,11 +235,31 @@ func TestUpdateGetDelete(t *testing.T) { }, }, } + _, err = provider.Apply(context.Background(), deployment, step, false) + assert.NotNil(t, err) +} +func TestGetFailed(t *testing.T) { + // NOTE: To run this test case successfully, you need to have Docker and Redis container running: + // docker run -d -p 5000:5000 --name mock_piccolo hbai/piccolo-mock:latest + // Befor this, TestPiccoloTargetProviderApply must be called. + cmd := exec.Command("podman", "ps", "-q", "-f", "name=mock_piccolo") + output, err := cmd.Output() assert.Nil(t, err) + if len(output) == 0 { + t.Skip("Skipping because mock_picc container is not exist") + } - // Get - components, err := provider.Get(context.Background(), model.DeploymentSpec{ + configMap := map[string]string{ + "name": "piccolo", + "url": "http://127.0.0.1:5000", + } + config, err := PiccoloTargetProviderConfigFromMap(configMap) + assert.Nil(t, err) + provider := PiccoloTargetProvider{} + err = provider.Init(config) + assert.Nil(t, err) + _, err = provider.Get(context.Background(), model.DeploymentSpec{ Instance: model.InstanceState{ Spec: &model.InstanceSpec{}, }, @@ -209,10 +267,10 @@ func TestUpdateGetDelete(t *testing.T) { Spec: &model.SolutionSpec{ Components: []model.ComponentSpec{ { - Name: "alpine-test", + Name: "notApplied", Type: "container", Properties: map[string]interface{}{ - "workload.name": "alpine:3.18", + "workload.name": "notApplied-test", }, }, }, @@ -222,112 +280,40 @@ func TestUpdateGetDelete(t *testing.T) { { Action: model.ComponentUpdate, Component: model.ComponentSpec{ - Name: "alpine-test", + Name: "notApplied", Type: "container", Properties: map[string]interface{}{ - "workload.name": "alpine:3.18", + "workload.name": "notApplied-test", }, }, }, }) - assert.Nil(t, err) - assert.Equal(t, 1, len(components)) - - // Delete - step = model.DeploymentStep{ - Components: []model.ComponentStep{ - { - Action: model.ComponentDelete, - Component: component, - }, - }, - } - _, err = provider.Apply(context.Background(), deployment, step, false) - assert.Nil(t, err) + assert.NotNil(t, err) } - -func TestApplyFailed(t *testing.T) { - testPiccoloProvider := os.Getenv("TEST_PICCOLO_ENABLED") - if testPiccoloProvider == "" { - t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") - } - config := PiccoloTargetProviderConfig{} - provider := PiccoloTargetProvider{} - err := provider.Init(config) +func TestDeleteFailed(t *testing.T) { + // NOTE: To run this test case successfully, you need to have Docker and Redis container running: + // docker run -d -p 5000:5000 --name mock_piccolo hbai/piccolo-mock:latest + cmd := exec.Command("podman", "ps", "-q", "-f", "name=mock_piccolo") + output, err := cmd.Output() assert.Nil(t, err) - - // invalid container image name - component := model.ComponentSpec{ - Name: "", - Type: "container", + if len(output) == 0 { + t.Skip("Skipping because mock_picc container is not exist") } - deployment := model.DeploymentSpec{ - Instance: model.InstanceState{ - Spec: &model.InstanceSpec{}, - }, - Solution: model.SolutionState{ - Spec: &model.SolutionSpec{ - Components: []model.ComponentSpec{component}, - }, - }, - } - step := model.DeploymentStep{ - Components: []model.ComponentStep{ - { - Action: model.ComponentUpdate, - Component: component, - }, - }, - } - - _, err = provider.Apply(context.Background(), deployment, step, false) - assert.NotNil(t, err) - // unknown container image - component = model.ComponentSpec{ - Name: "abcd:latest", - Type: "container", - Properties: map[string]interface{}{ - "workload.name": "abc:latest", - }, - } - deployment = model.DeploymentSpec{ - Instance: model.InstanceState{ - Spec: &model.InstanceSpec{}, - }, - Solution: model.SolutionState{ - Spec: &model.SolutionSpec{ - Components: []model.ComponentSpec{component}, - }, - }, - } - step = model.DeploymentStep{ - Components: []model.ComponentStep{ - { - Action: model.ComponentUpdate, - Component: component, - }, - }, - } - _, err = provider.Apply(context.Background(), deployment, step, false) - assert.NotNil(t, err) -} - -func TestApplyAlreadyRunning(t *testing.T) { - testPiccoloProvider := os.Getenv("TEST_PICCOLO_ENABLED") - if testPiccoloProvider == "" { - t.Skip("Skipping because TEST_PICCOLO_PROVIDER enviornment variable is not set") + configMap := map[string]string{ + "name": "piccolo", + "url": "http://127.0.0.1:5000", } - config := PiccoloTargetProviderConfig{} + config, err := PiccoloTargetProviderConfigFromMap(configMap) + assert.Nil(t, err) provider := PiccoloTargetProvider{} - err := provider.Init(config) + err = provider.Init(config) assert.Nil(t, err) - component := model.ComponentSpec{ - Name: "alpine-test", + Name: "notApplied", Type: "container", Properties: map[string]interface{}{ - "workload.name": "alpine:3.18", + "workload.name": "notApplied-test", }, } deployment := model.DeploymentSpec{ @@ -343,17 +329,13 @@ func TestApplyAlreadyRunning(t *testing.T) { step := model.DeploymentStep{ Components: []model.ComponentStep{ { - Action: model.ComponentUpdate, + Action: model.ComponentDelete, Component: component, }, }, } _, err = provider.Apply(context.Background(), deployment, step, false) - assert.Nil(t, err) - - // already running - _, err = provider.Apply(context.Background(), deployment, step, false) - assert.Nil(t, err) + assert.NotNil(t, err) } func TestConformanceSuite(t *testing.T) { diff --git a/docs/mocks/piccolo-mock/app.py b/docs/mocks/piccolo-mock/app.py index e8402576c..2ee03437d 100644 --- a/docs/mocks/piccolo-mock/app.py +++ b/docs/mocks/piccolo-mock/app.py @@ -8,51 +8,45 @@ app = Flask(__name__) # Mock storage for scenarios: keyed by workload_type, with each value being a list of workload names -scenarios = {"binary": ["plc-controller"]} +# scenarios = {"binary": ["plc-controller"]} +scenarios = [] -@app.route('//', methods=['GET']) -def get_workload(workload_type, workload_name): +@app.route('/scenario/', methods=['GET']) +def get_workload(workload_name): """ This endpoint simulates getting a workload from Piccolo. """ # Search for workload_name within the specified workload_type - if workload_type in scenarios and workload_name in scenarios[workload_type]: + if workload_name in scenarios: return jsonify({"message": "Workload found"}), 200 else: return jsonify({"message": "Workload not found"}), 404 -@app.route('/create-scenario/', methods=['POST']) -def create_scenario(component_name): +@app.route('/scenario', methods=['POST']) +def create_scenario(): """ This endpoint simulates creating a scenario. """ - req_body = request.get_json() or {} - - # Extract workload_type from request body, or default to "binary" - workload_type = req_body.get("properties", {}).get("workload_type", "binary") - - # Add workload to scenarios dictionary - if workload_type not in scenarios: - scenarios[workload_type] = [] + # Extract workload_name from request body, or default to "" + workload_name = request.data.decode('utf-8') or '' # Add component_name to the appropriate workload_type list if it doesn't already exist - if component_name not in scenarios[workload_type]: - scenarios[workload_type].append(component_name) + if workload_name not in scenarios: + scenarios.append(workload_name) - return jsonify({"message": f"Scenario '{component_name}' of type '{workload_type}' created successfully"}), 201 + return jsonify({"message": f"Scenario '{workload_name}' created successfully"}), 201 -@app.route('/delete-scenario/', methods=['DELETE']) -def delete_scenario(component_name): +@app.route('/scenario/', methods=['DELETE']) +def delete_scenario(workload_name): """ This endpoint simulates deleting a scenario. """ # Find and delete the component_name from all workload_type lists - for workload_type, workload_list in scenarios.items(): - if component_name in workload_list: - workload_list.remove(component_name) - return jsonify({"message": f"Scenario '{component_name}' deleted successfully"}), 200 - - return jsonify({"error": f"Scenario '{component_name}' not found"}), 404 + if workload_name in scenarios: + scenarios.remove(workload_name) + return jsonify({"message": f"Scenario '{workload_name}' deleted successfully"}), 200 + else: + return jsonify({"error": f"Scenario '{workload_name}' not found"}), 404 if __name__ == '__main__': app.run(port=5000, debug=True)