diff --git a/api/coverage.html b/api/coverage.html
new file mode 100644
index 000000000..dff1ca5ed
--- /dev/null
+++ b/api/coverage.html
@@ -0,0 +1,23187 @@
+
+
+
+
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package activations
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "time"
+
+ "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"
+)
+
+const (
+ // DefaultRetentionInMinutes is the default time to cleanup completed activations
+ DefaultRetentionInMinutes = 1440
+)
+
+type ActivationsCleanupManager struct {
+ ActivationsManager
+ RetentionInMinutes int
+}
+
+func (s *ActivationsCleanupManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error {
+ err := s.ActivationsManager.Init(context, config, providers)
+ if err != nil {
+ return err
+ }
+
+ // Set activation cleanup interval after they are done. If not set, use default 60 minutes.
+ if val, ok := config.Properties["RetentionInMinutes"]; ok {
+ s.RetentionInMinutes, err = strconv.Atoi(val)
+ if err != nil {
+ s.RetentionInMinutes = DefaultRetentionInMinutes
+ }
+ } else {
+ s.RetentionInMinutes = DefaultRetentionInMinutes
+ }
+ log.Info("M (Activation Cleanup): Initialize RetentionInMinutes as " + fmt.Sprint(s.RetentionInMinutes))
+ return nil
+}
+
+func (s *ActivationsCleanupManager) Enabled() bool {
+ return true
+}
+
+func (s *ActivationsCleanupManager) Poll() []error {
+ log.Info("M (Activation Cleanup): Polling activations")
+ activations, err := s.ActivationsManager.ListSpec(context.Background())
+ if err != nil {
+ return []error{err}
+ }
+ ret := []error{}
+ for _, activation := range activations {
+ if activation.Status.Status != v1alpha2.Done {
+ continue
+ }
+ if activation.Status.UpdateTime == "" {
+ // Ugrade scenario: update time is not set for activations created before. Set it to now and the activation will be deleted later.
+ // UpdateTime will be set in ReportStatus function
+ err = s.ActivationsManager.ReportStatus(context.Background(), activation.Id, *activation.Status)
+ if err != nil {
+ // Delete activation immediately if update time cannot be set? Cx may be confused why activations disappeared
+ // Just leave those activations as it is and let Cx delete them manually
+ log.Error("M (Activation Cleanup): Cannot set update time for activation "+activation.Id+" since update time cannot be set: %+v", err)
+ ret = append(ret, err)
+ }
+ continue
+ }
+
+ // Check update time of completed activations.
+ updateTime, err := time.Parse(time.RFC3339, activation.Status.UpdateTime)
+ if err != nil {
+ // TODO: should not happen, force update time to Time.Now() ?
+ log.Info("M (Activation Cleanup): Cannot parse update time of " + activation.Id)
+ ret = append(ret, err)
+ }
+ duration := time.Since(updateTime)
+ if duration > time.Duration(s.RetentionInMinutes)*time.Minute {
+ log.Info("M (Activation Cleanup): Deleting activation " + activation.Id + " since it has completed for " + duration.String())
+ err = s.ActivationsManager.DeleteSpec(context.Background(), activation.Id)
+ if err != nil {
+ ret = append(ret, err)
+ }
+ }
+ }
+ return ret
+}
+
+func (s *ActivationsCleanupManager) Reconcil() []error {
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package activations
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
+ observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var lock sync.Mutex
+
+var log = logger.NewLogger("coa.runtime")
+
+type ActivationsManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+func (s *ActivationsManager) 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 (m *ActivationsManager) GetSpec(ctx context.Context, name string) (model.ActivationState, error) {
+ ctx, span := observability.StartSpan("Activations Manager", ctx, &map[string]string{
+ "method": "GetSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ getRequest := states.GetRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.WorkflowGroup,
+ "resource": "activations",
+ },
+ }
+ entry, err := m.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return model.ActivationState{}, err
+ }
+
+ ret, err := getActivationState(name, entry.Body, entry.ETag)
+ if err != nil {
+ return model.ActivationState{}, err
+ }
+ return ret, nil
+}
+
+func getActivationState(id string, body interface{}, etag string) (model.ActivationState, error) {
+ dict := body.(map[string]interface{})
+ spec := dict["spec"]
+ status := dict["status"]
+ j, _ := json.Marshal(spec)
+ var rSpec model.ActivationSpec
+ err := json.Unmarshal(j, &rSpec)
+ if err != nil {
+ return model.ActivationState{}, err
+ }
+ j, _ = json.Marshal(status)
+ var rStatus model.ActivationStatus
+ err = json.Unmarshal(j, &rStatus)
+ if err != nil {
+ return model.ActivationState{}, err
+ }
+ rSpec.Generation = etag
+ state := model.ActivationState{
+ Id: id,
+ Spec: &rSpec,
+ Status: &rStatus,
+ }
+ return state, nil
+}
+
+func (m *ActivationsManager) UpsertSpec(ctx context.Context, name string, spec model.ActivationSpec) error {
+ ctx, span := observability.StartSpan("Activations Manager", ctx, &map[string]string{
+ "method": "UpsertSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ upsertRequest := states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: name,
+ Body: map[string]interface{}{
+ "apiVersion": model.WorkflowGroup + "/v1",
+ "kind": "Activation",
+ "metadata": map[string]interface{}{
+ "name": name,
+ },
+ "spec": spec,
+ },
+ ETag: spec.Generation,
+ },
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Activation", "metadata": {"name": "${{$activation()}}"}}`, model.WorkflowGroup),
+ "scope": "",
+ "group": model.WorkflowGroup,
+ "version": "v1",
+ "resource": "activations",
+ },
+ }
+ _, err = m.StateProvider.Upsert(ctx, upsertRequest)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *ActivationsManager) DeleteSpec(ctx context.Context, name string) error {
+ ctx, span := observability.StartSpan("Activations Manager", ctx, &map[string]string{
+ "method": "DeleteSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = m.StateProvider.Delete(ctx, states.DeleteRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "scope": "",
+ "group": model.WorkflowGroup,
+ "version": "v1",
+ "resource": "activations",
+ },
+ })
+ return err
+}
+
+func (t *ActivationsManager) ListSpec(ctx context.Context) ([]model.ActivationState, error) {
+ ctx, span := observability.StartSpan("Activations Manager", ctx, &map[string]string{
+ "method": "ListSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ listRequest := states.ListRequest{
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.WorkflowGroup,
+ "resource": "activations",
+ },
+ }
+ solutions, _, err := t.StateProvider.List(ctx, listRequest)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]model.ActivationState, 0)
+ for _, t := range solutions {
+ var rt model.ActivationState
+ rt, err = getActivationState(t.ID, t.Body, t.ETag)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, rt)
+ }
+ return ret, nil
+}
+func (t *ActivationsManager) ReportStatus(ctx context.Context, name string, current model.ActivationStatus) error {
+ ctx, span := observability.StartSpan("Activations Manager", ctx, &map[string]string{
+ "method": "ReportStatus",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+ lock.Lock()
+ defer lock.Unlock()
+ getRequest := states.GetRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.WorkflowGroup,
+ "resource": "activations",
+ },
+ }
+ entry, err := t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ dict := entry.Body.(map[string]interface{})
+ delete(dict, "spec")
+ current.UpdateTime = time.Now().Format(time.RFC3339)
+ dict["status"] = current
+ entry.Body = dict
+ upsertRequest := states.UpsertRequest{
+ Value: entry,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.WorkflowGroup,
+ "resource": "activations",
+ },
+ }
+ _, err = t.StateProvider.Upsert(ctx, upsertRequest)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package campaigns
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
+ observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+)
+
+type CampaignsManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+func (s *CampaignsManager) 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
+}
+
+// GetCampaign retrieves a CampaignSpec object by name
+func (m *CampaignsManager) GetSpec(ctx context.Context, name string) (model.CampaignState, error) {
+ ctx, span := observability.StartSpan("Campaigns Manager", ctx, &map[string]string{
+ "method": "GetSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ getRequest := states.GetRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.WorkflowGroup,
+ "resource": "campaigns",
+ },
+ }
+ entry, err := m.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return model.CampaignState{}, err
+ }
+
+ ret, err := getCampaignState(name, entry.Body)
+ if err != nil {
+ return model.CampaignState{}, err
+ }
+ return ret, nil
+}
+
+func getCampaignState(id string, body interface{}) (model.CampaignState, error) {
+ dict := body.(map[string]interface{})
+ spec := dict["spec"]
+
+ j, _ := json.Marshal(spec)
+ var rSpec model.CampaignSpec
+ err := json.Unmarshal(j, &rSpec)
+ if err != nil {
+ return model.CampaignState{}, err
+ }
+ state := model.CampaignState{
+ Id: id,
+ Spec: &rSpec,
+ }
+ return state, nil
+}
+
+func (m *CampaignsManager) UpsertSpec(ctx context.Context, name string, spec model.CampaignSpec) error {
+ ctx, span := observability.StartSpan("Campaigns Manager", ctx, &map[string]string{
+ "method": "UpsertSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ upsertRequest := states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: name,
+ Body: map[string]interface{}{
+ "apiVersion": model.WorkflowGroup + "/v1",
+ "kind": "Campaign",
+ "metadata": map[string]interface{}{
+ "name": name,
+ },
+ "spec": spec,
+ },
+ },
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Campaign", "metadata": {"name": "${{$campaign()}}"}}`, model.WorkflowGroup),
+ "scope": "",
+ "group": model.WorkflowGroup,
+ "version": "v1",
+ "resource": "campaigns",
+ },
+ }
+ _, err = m.StateProvider.Upsert(ctx, upsertRequest)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *CampaignsManager) DeleteSpec(ctx context.Context, name string) error {
+ ctx, span := observability.StartSpan("Campaigns Manager", ctx, &map[string]string{
+ "method": "DeleteSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = m.StateProvider.Delete(ctx, states.DeleteRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "scope": "",
+ "group": model.WorkflowGroup,
+ "version": "v1",
+ "resource": "campaigns",
+ },
+ })
+ return err
+}
+
+func (t *CampaignsManager) ListSpec(ctx context.Context) ([]model.CampaignState, error) {
+ ctx, span := observability.StartSpan("Campaigns Manager", ctx, &map[string]string{
+ "method": "ListSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ listRequest := states.ListRequest{
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.WorkflowGroup,
+ "resource": "campaigns",
+ },
+ }
+ solutions, _, err := t.StateProvider.List(ctx, listRequest)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]model.CampaignState, 0)
+ for _, t := range solutions {
+ var rt model.CampaignState
+ rt, err = getCampaignState(t.ID, t.Body)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, rt)
+ }
+ return ret, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package configs
+
+import (
+ "fmt"
+ "strings"
+
+ "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/config"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var log = logger.NewLogger("coa.runtime")
+
+type ConfigsManager struct {
+ managers.Manager
+ ConfigProviders map[string]config.IConfigProvider
+ Precedence []string
+}
+
+func (s *ConfigsManager) Init(context *contexts.VendorContext, cfg managers.ManagerConfig, providers map[string]providers.IProvider) error {
+ log.Debug(" M (Config): Init")
+ err := s.Manager.Init(context, cfg, providers)
+ if err != nil {
+ return err
+ }
+ s.ConfigProviders = make(map[string]config.IConfigProvider)
+ for key, provider := range providers {
+ if cProvider, ok := provider.(config.IConfigProvider); ok {
+ s.ConfigProviders[key] = cProvider
+ }
+ }
+ if val, ok := cfg.Properties["precedence"]; ok {
+ s.Precedence = strings.Split(val, ",")
+ }
+ if len(s.ConfigProviders) == 0 {
+ log.Error(" M (Config): No config providers found")
+ return v1alpha2.NewCOAError(nil, "No config providers found", v1alpha2.BadConfig)
+ }
+ if len(s.ConfigProviders) > 0 && len(s.Precedence) < len(s.ConfigProviders) && len(s.ConfigProviders) > 1 {
+ log.Error(" M (Config): Not enough precedence values")
+ return v1alpha2.NewCOAError(nil, "Not enough precedence values", v1alpha2.BadConfig)
+ }
+ for _, key := range s.Precedence {
+ if _, ok := s.ConfigProviders[key]; !ok {
+ log.Error(" M (Config): Invalid precedence value: %s", key)
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid precedence value: %s", key), v1alpha2.BadConfig)
+ }
+ }
+ return nil
+}
+func (s *ConfigsManager) Get(object string, field string, overlays []string, localContext interface{}) (interface{}, error) {
+ if strings.Index(object, ":") > 0 {
+ parts := strings.Split(object, ":")
+ if len(parts) != 2 {
+ return "", v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid object: %s", object), v1alpha2.BadRequest)
+ }
+ if provider, ok := s.ConfigProviders[parts[0]]; ok {
+ if field == "" {
+ configObj, err := s.getObjectWithOverlay(provider, parts[1], overlays, localContext)
+ if err != nil {
+ return "", err
+ }
+ return configObj, nil
+ } else {
+ return s.getWithOverlay(provider, parts[1], field, overlays, localContext)
+ }
+ }
+ return "", v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid provider: %s", parts[0]), v1alpha2.BadRequest)
+ }
+ if len(s.ConfigProviders) == 1 {
+ for _, provider := range s.ConfigProviders {
+ if field == "" {
+ configObj, err := s.getObjectWithOverlay(provider, object, overlays, localContext)
+ if err != nil {
+ return "", err
+ }
+ return configObj, nil
+ } else {
+ if value, err := s.getWithOverlay(provider, object, field, overlays, localContext); err == nil {
+ return value, nil
+ } else {
+ return "", err
+ }
+ }
+ }
+ }
+ for _, key := range s.Precedence {
+ if provider, ok := s.ConfigProviders[key]; ok {
+ if field == "" {
+ configObj, err := s.getObjectWithOverlay(provider, object, overlays, localContext)
+ if err != nil {
+ return "", err
+ }
+ return configObj, nil
+ } else {
+ if value, err := s.getWithOverlay(provider, object, field, overlays, localContext); err == nil {
+ return value, nil
+ }
+ }
+ }
+ }
+ return "", v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid config object or key: %s, %s", object, field), v1alpha2.BadRequest)
+}
+func (s *ConfigsManager) getWithOverlay(provider config.IConfigProvider, object string, field string, overlays []string, localContext interface{}) (interface{}, error) {
+ if len(overlays) > 0 {
+ for _, overlay := range overlays {
+ if overlayObject, err := provider.Read(overlay, field, localContext); err == nil {
+ return overlayObject, nil
+ }
+ }
+ }
+ return provider.Read(object, field, localContext)
+}
+
+func (s *ConfigsManager) GetObject(object string, overlays []string, localContext interface{}) (map[string]interface{}, error) {
+ if strings.Index(object, ":") > 0 {
+ parts := strings.Split(object, ":")
+ if len(parts) != 2 {
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid object: %s", object), v1alpha2.BadRequest)
+ }
+ if provider, ok := s.ConfigProviders[parts[0]]; ok {
+ return s.getObjectWithOverlay(provider, parts[1], overlays, localContext)
+ }
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid provider: %s", parts[0]), v1alpha2.BadRequest)
+ }
+ if len(s.ConfigProviders) == 1 {
+ for _, provider := range s.ConfigProviders {
+ if value, err := s.getObjectWithOverlay(provider, object, overlays, localContext); err == nil {
+ return value, nil
+ } else {
+ return nil, err
+ }
+ }
+ }
+ for _, key := range s.Precedence {
+ if provider, ok := s.ConfigProviders[key]; ok {
+ if value, err := s.getObjectWithOverlay(provider, object, overlays, localContext); err == nil {
+ return value, nil
+ }
+ }
+ }
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid config object: %s", object), v1alpha2.BadRequest)
+}
+func (s *ConfigsManager) getObjectWithOverlay(provider config.IConfigProvider, object string, overlays []string, localContext interface{}) (map[string]interface{}, error) {
+ if len(overlays) > 0 {
+ for _, overlay := range overlays {
+ if overlayObject, err := provider.ReadObject(overlay, localContext); err == nil {
+ return overlayObject, nil
+ }
+ }
+ }
+ return provider.ReadObject(object, localContext)
+}
+func (s *ConfigsManager) Set(object string, field string, value interface{}) error {
+ if strings.Index(object, ":") > 0 {
+ parts := strings.Split(object, ":")
+ if len(parts) != 2 {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid object: %s", object), v1alpha2.BadRequest)
+ }
+ if provider, ok := s.ConfigProviders[parts[0]]; ok {
+ return provider.Set(parts[1], field, value)
+ }
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid provider: %s", parts[0]), v1alpha2.BadRequest)
+ }
+ if len(s.ConfigProviders) == 1 {
+ for _, provider := range s.ConfigProviders {
+ return provider.Set(object, field, value)
+ }
+ }
+ for _, key := range s.Precedence {
+ if provider, ok := s.ConfigProviders[key]; ok {
+ if err := provider.Set(object, field, value); err == nil {
+ return nil
+ }
+ }
+ }
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid config object or key: %s, %s", object, field), v1alpha2.BadRequest)
+}
+func (s *ConfigsManager) SetObject(object string, values map[string]interface{}) error {
+ if strings.Index(object, ":") > 0 {
+ parts := strings.Split(object, ":")
+ if len(parts) != 2 {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid object: %s", object), v1alpha2.BadRequest)
+ }
+ if provider, ok := s.ConfigProviders[parts[0]]; ok {
+ return provider.SetObject(parts[1], values)
+ }
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid provider: %s", parts[0]), v1alpha2.BadRequest)
+ }
+ if len(s.ConfigProviders) == 1 {
+ for _, provider := range s.ConfigProviders {
+ return provider.SetObject(object, values)
+ }
+ }
+ for _, key := range s.Precedence {
+ if provider, ok := s.ConfigProviders[key]; ok {
+ if err := provider.SetObject(object, values); err == nil {
+ return nil
+ }
+ }
+ }
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid config object: %s", object), v1alpha2.BadRequest)
+}
+func (s *ConfigsManager) Delete(object string, field string) error {
+ if strings.Index(object, ":") > 0 {
+ parts := strings.Split(object, ":")
+ if len(parts) != 2 {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid object: %s", object), v1alpha2.BadRequest)
+ }
+ if provider, ok := s.ConfigProviders[parts[0]]; ok {
+ return provider.Remove(parts[1], field)
+ }
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid provider: %s", parts[0]), v1alpha2.BadRequest)
+ }
+ if len(s.ConfigProviders) == 1 {
+ for _, provider := range s.ConfigProviders {
+ return provider.Remove(object, field)
+ }
+ }
+ for _, key := range s.Precedence {
+ if provider, ok := s.ConfigProviders[key]; ok {
+ if err := provider.Remove(object, field); err == nil {
+ return nil
+ }
+ }
+ }
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid config object or key: %s, %s", object, field), v1alpha2.BadRequest)
+}
+func (s *ConfigsManager) DeleteObject(object string) error {
+ if strings.Index(object, ":") > 0 {
+ parts := strings.Split(object, ":")
+ if len(parts) != 2 {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid object: %s", object), v1alpha2.BadRequest)
+ }
+ if provider, ok := s.ConfigProviders[parts[0]]; ok {
+ return provider.RemoveObject(parts[1])
+ }
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid provider: %s", parts[0]), v1alpha2.BadRequest)
+ }
+ if len(s.ConfigProviders) == 1 {
+ for _, provider := range s.ConfigProviders {
+ return provider.RemoveObject(object)
+ }
+ }
+ for _, key := range s.Precedence {
+ if provider, ok := s.ConfigProviders[key]; ok {
+ if err := provider.RemoveObject(object); err == nil {
+ return nil
+ }
+ }
+ }
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Invalid config object: %s", object), v1alpha2.BadRequest)
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package devices
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+
+ observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+)
+
+type DevicesManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+func (s *DevicesManager) 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 *DevicesManager) DeleteSpec(ctx context.Context, name string) error {
+ ctx, span := observability.StartSpan("Devices Manager", ctx, &map[string]string{
+ "method": "DeleteSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = t.StateProvider.Delete(ctx, states.DeleteRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "scope": "",
+ "group": model.FabricGroup,
+ "version": "v1",
+ "resource": "devices",
+ },
+ })
+ return err
+}
+
+func (t *DevicesManager) UpsertSpec(ctx context.Context, name string, spec model.DeviceSpec) error {
+ ctx, span := observability.StartSpan("Devices Manager", ctx, &map[string]string{
+ "method": "UpsertSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ upsertRequest := states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: name,
+ Body: map[string]interface{}{
+ "apiVersion": model.FabricGroup + "/v1",
+ "kind": "device",
+ "metadata": map[string]interface{}{
+ "name": name,
+ },
+ "spec": spec,
+ },
+ },
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion": "%s/v1", "kind": "Device", "metadata": {"name": "${{$device()}}"}}`, model.FabricGroup),
+ "scope": "",
+ "group": model.FabricGroup,
+ "version": "v1",
+ "resource": "devices",
+ },
+ }
+ _, err = t.StateProvider.Upsert(ctx, upsertRequest)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (t *DevicesManager) ListSpec(ctx context.Context) ([]model.DeviceState, error) {
+ ctx, span := observability.StartSpan("Devices Manager", ctx, &map[string]string{
+ "method": "ListSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ listRequest := states.ListRequest{
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FabricGroup,
+ "resource": "devices",
+ },
+ }
+ solutions, _, err := t.StateProvider.List(ctx, listRequest)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]model.DeviceState, 0)
+ for _, t := range solutions {
+ var rt model.DeviceState
+ rt, err = getDeviceState(t.ID, t.Body)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, rt)
+ }
+ return ret, nil
+}
+
+func getDeviceState(id string, body interface{}) (model.DeviceState, error) {
+ dict := body.(map[string]interface{})
+ spec := dict["spec"]
+
+ j, _ := json.Marshal(spec)
+ var rSpec model.DeviceSpec
+ err := json.Unmarshal(j, &rSpec)
+ if err != nil {
+ return model.DeviceState{}, err
+ }
+ state := model.DeviceState{
+ Id: id,
+ Spec: &rSpec,
+ }
+ return state, nil
+}
+
+func (t *DevicesManager) GetSpec(ctx context.Context, id string) (model.DeviceState, error) {
+ ctx, span := observability.StartSpan("Devices Manager", ctx, &map[string]string{
+ "method": "GetSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ getRequest := states.GetRequest{
+ ID: id,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FabricGroup,
+ "resource": "devices",
+ },
+ }
+ target, err := t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return model.DeviceState{}, err
+ }
+
+ ret, err := getDeviceState(id, target.Body)
+ if err != nil {
+ return model.DeviceState{}, err
+ }
+ return ret, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package instances
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+
+ observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+)
+
+type InstancesManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+func (s *InstancesManager) 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 *InstancesManager) DeleteSpec(ctx context.Context, name string, scope string) error {
+ ctx, span := observability.StartSpan("Instances Manager", ctx, &map[string]string{
+ "method": "DeleteSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = t.StateProvider.Delete(ctx, states.DeleteRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "scope": scope,
+ "group": model.SolutionGroup,
+ "version": "v1",
+ "resource": "instances",
+ },
+ })
+ return err
+}
+
+func (t *InstancesManager) UpsertSpec(ctx context.Context, name string, spec model.InstanceSpec, scope string) error {
+ ctx, span := observability.StartSpan("Instances Manager", ctx, &map[string]string{
+ "method": "UpsertSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ upsertRequest := states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: name,
+ Body: map[string]interface{}{
+ "apiVersion": model.SolutionGroup + "/v1",
+ "kind": "Instance",
+ "metadata": map[string]interface{}{
+ "name": name,
+ },
+ "spec": spec,
+ },
+ ETag: spec.Generation,
+ },
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Instance", "metadata": {"name": "${{$instance()}}"}}`, model.SolutionGroup),
+ "scope": scope,
+ "group": model.SolutionGroup,
+ "version": "v1",
+ "resource": "instances",
+ },
+ }
+ _, err = t.StateProvider.Upsert(ctx, upsertRequest)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (t *InstancesManager) ListSpec(ctx context.Context, scope string) ([]model.InstanceState, error) {
+ ctx, span := observability.StartSpan("Instances Manager", ctx, &map[string]string{
+ "method": "ListSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ listRequest := states.ListRequest{
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.SolutionGroup,
+ "resource": "instances",
+ "scope": scope,
+ },
+ }
+ instances, _, err := t.StateProvider.List(ctx, listRequest)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]model.InstanceState, 0)
+ for _, t := range instances {
+ var rt model.InstanceState
+ rt, err = getInstanceState(t.ID, t.Body, t.ETag)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, rt)
+ }
+ return ret, nil
+}
+
+func getInstanceState(id string, body interface{}, etag string) (model.InstanceState, error) {
+ dict := body.(map[string]interface{})
+ spec := dict["spec"]
+ status := dict["status"]
+
+ j, _ := json.Marshal(spec)
+ var rSpec model.InstanceSpec
+ err := json.Unmarshal(j, &rSpec)
+ if err != nil {
+ return model.InstanceState{}, err
+ }
+
+ j, _ = json.Marshal(status)
+ var rStatus map[string]interface{}
+ err = json.Unmarshal(j, &rStatus)
+ if err != nil {
+ return model.InstanceState{}, err
+ }
+ j, _ = json.Marshal(rStatus["properties"])
+ var rProperties map[string]string
+ err = json.Unmarshal(j, &rProperties)
+ if err != nil {
+ return model.InstanceState{}, err
+ }
+ rSpec.Generation = etag
+
+ scope, exist := dict["scope"]
+ var s string
+ if !exist {
+ s = "default"
+ } else {
+ s = scope.(string)
+ }
+
+ state := model.InstanceState{
+ Id: id,
+ Scope: s,
+ Spec: &rSpec,
+ Status: rProperties,
+ }
+ return state, nil
+}
+
+func (t *InstancesManager) GetSpec(ctx context.Context, id string, scope string) (model.InstanceState, error) {
+ ctx, span := observability.StartSpan("Instances Manager", ctx, &map[string]string{
+ "method": "GetSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ getRequest := states.GetRequest{
+ ID: id,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.SolutionGroup,
+ "resource": "instances",
+ "scope": scope,
+ },
+ }
+ instance, err := t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return model.InstanceState{}, err
+ }
+
+ ret, err := getInstanceState(id, instance.Body, instance.ETag)
+ if err != nil {
+ return model.InstanceState{}, err
+ }
+ return ret, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package jobs
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "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/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
+ observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var log = logger.NewLogger("coa.runtime")
+
+type JobsManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+type LastSuccessTime struct {
+ Time time.Time `json:"time"`
+}
+
+func (s *JobsManager) 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 (s *JobsManager) Enabled() bool {
+ return s.Config.Properties["poll.enabled"] == "true" || s.Config.Properties["schedule.enabled"] == "true"
+}
+
+func (s *JobsManager) pollObjects() []error {
+ context, span := observability.StartSpan("Job Manager", context.Background(), &map[string]string{
+ "method": "pollObjects",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ baseUrl, err := utils.GetString(s.Manager.Config.Properties, "baseUrl")
+ if err != nil {
+ return []error{err}
+ }
+ user, err := utils.GetString(s.Manager.Config.Properties, "user")
+ if err != nil {
+ return []error{err}
+ }
+ password, err := utils.GetString(s.Manager.Config.Properties, "password")
+ if err != nil {
+ return []error{err}
+ }
+ interval := utils.ReadInt32(s.Manager.Config.Properties, "interval", 0)
+ if interval == 0 {
+ return nil
+ }
+ instances, err := utils.GetInstancesForAllScope(context, baseUrl, user, password)
+ if err != nil {
+ fmt.Println(err.Error())
+ return []error{err}
+ }
+ for _, instance := range instances {
+ var entry states.StateEntry
+ entry, err = s.StateProvider.Get(context, states.GetRequest{
+ ID: "i_" + instance.Id,
+ })
+ needsPub := true
+ if err == nil {
+ if stamp, ok := entry.Body.(LastSuccessTime); ok {
+ if time.Since(stamp.Time) > time.Duration(interval)*time.Second { //TODO: compare object hash as well?
+ needsPub = true
+ } else {
+ needsPub = false
+ }
+ }
+ }
+ if needsPub {
+ s.Context.Publish("job", v1alpha2.Event{
+ Metadata: map[string]string{
+ "objectType": "instance",
+ },
+ Body: v1alpha2.JobData{
+ Id: instance.Id,
+ Action: "UPDATE",
+ },
+ })
+ }
+ }
+ targets, err := utils.GetTargetsForAllScope(context, baseUrl, user, password)
+ if err != nil {
+ fmt.Println(err.Error())
+ return []error{err}
+ }
+ for _, target := range targets {
+ var entry states.StateEntry
+ entry, err = s.StateProvider.Get(context, states.GetRequest{
+ ID: "t_" + target.Id,
+ })
+ needsPub := true
+ if err == nil {
+ var stamp LastSuccessTime
+ jData, _ := json.Marshal(entry.Body)
+ err = json.Unmarshal(jData, &stamp)
+ if err == nil {
+ if time.Since(stamp.Time) > time.Duration(interval)*time.Second { //TODO: compare object hash as well?
+ needsPub = true
+ } else {
+ needsPub = false
+ }
+ }
+ }
+ if needsPub {
+ s.Context.Publish("job", v1alpha2.Event{
+ Metadata: map[string]string{
+ "objectType": "target",
+ },
+ Body: v1alpha2.JobData{
+ Id: target.Id,
+ Action: "UPDATE",
+ },
+ })
+ }
+ }
+
+ return nil
+}
+func (s *JobsManager) Poll() []error {
+ // TODO: do these in parallel?
+ if s.Config.Properties["poll.enabled"] == "true" {
+ errors := s.pollObjects()
+ if len(errors) > 0 {
+ return errors
+ }
+ }
+ if s.Config.Properties["schedule.enabled"] == "true" {
+ errors := s.pollSchedules()
+ if len(errors) > 0 {
+ return errors
+ }
+ }
+ return nil
+}
+
+func (s *JobsManager) pollSchedules() []error {
+ context, span := observability.StartSpan("Job Manager", context.Background(), &map[string]string{
+ "method": "pollSchedules",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ //TODO: use filters and continue tokens
+ list, _, err := s.StateProvider.List(context, states.ListRequest{})
+ if err != nil {
+ return []error{err}
+ }
+
+ for _, entry := range list {
+ var activationData v1alpha2.ActivationData
+ entryData, _ := json.Marshal(entry.Body)
+ err = json.Unmarshal(entryData, &activationData)
+ if err != nil {
+ return []error{err}
+ }
+ if activationData.Schedule != nil {
+ var fire bool
+ fire, err = activationData.Schedule.ShouldFireNow()
+ if err != nil {
+ return []error{err}
+ }
+ if fire {
+ activationData.Schedule = nil
+ err = s.StateProvider.Delete(context, states.DeleteRequest{
+ ID: entry.ID,
+ })
+ if err != nil {
+ return []error{err}
+ }
+ s.Context.Publish("trigger", v1alpha2.Event{
+ Body: activationData,
+ })
+ }
+ }
+ }
+ return nil
+}
+
+func (s *JobsManager) Reconcil() []error {
+ return nil
+}
+func (s *JobsManager) HandleHeartBeatEvent(ctx context.Context, event v1alpha2.Event) error {
+ ctx, span := observability.StartSpan("Job Manager", ctx, &map[string]string{
+ "method": "HandleHeartBeatEvent",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ var heartbeat v1alpha2.HeartBeatData
+ jData, _ := json.Marshal(event.Body)
+ err = json.Unmarshal(jData, &heartbeat)
+ if err != nil {
+ err = v1alpha2.NewCOAError(nil, "event body is not a heart beat", v1alpha2.BadRequest)
+ return err
+ }
+ // TODO: the heart beat data should contain a "finished" field so data can be cleared
+ _, err = s.StateProvider.Upsert(ctx, states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: "h_" + heartbeat.JobId,
+ Body: heartbeat,
+ },
+ })
+ return err
+}
+
+func (s *JobsManager) DelayOrSkipJob(ctx context.Context, objectType string, job v1alpha2.JobData) error {
+ ctx, span := observability.StartSpan("Job Manager", ctx, &map[string]string{
+ "method": "DelayOrSkipJob",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ key := "h_" + job.Id
+ if objectType == "target" {
+ key = fmt.Sprintf("h_%s-%s", "target-runtime", job.Id)
+ }
+ //check if a manager is working on the job
+ entry, err := s.StateProvider.Get(ctx, states.GetRequest{
+ ID: key,
+ })
+ if err != nil {
+ if !v1alpha2.IsNotFound(err) {
+ return err
+ }
+ return nil // no heartbeat
+ }
+ var heartbeat v1alpha2.HeartBeatData
+ jData, _ := json.Marshal(entry.Body)
+ err = json.Unmarshal(jData, &heartbeat)
+ if err != nil {
+ return err
+ }
+ if time.Since(heartbeat.Time) > time.Duration(60)*time.Second { //TODO: make this configurable
+ // heartbeat is too old
+ return nil
+ }
+ // job.Action is upper case and heartbeat.Action is lower case, use case insensitive comparison
+ if strings.EqualFold(job.Action, "delete") && strings.EqualFold(heartbeat.Action, "update") {
+ err = v1alpha2.NewCOAError(nil, "delete job is delayed", v1alpha2.Delayed)
+ return err
+ }
+ err = v1alpha2.NewCOAError(nil, "existing job in progress", v1alpha2.Untouched)
+ return err
+}
+func (s *JobsManager) HandleScheduleEvent(ctx context.Context, event v1alpha2.Event) error {
+ ctx, span := observability.StartSpan("Job Manager", ctx, &map[string]string{
+ "method": "HandleScheduleEvent",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ var activationData v1alpha2.ActivationData
+ jData, _ := json.Marshal(event.Body)
+ err = json.Unmarshal(jData, &activationData)
+ if err != nil {
+ return v1alpha2.NewCOAError(nil, "event body is not a activation data", v1alpha2.BadRequest)
+ }
+ key := fmt.Sprintf("sch_%s-%s", activationData.Campaign, activationData.Activation)
+ _, err = s.StateProvider.Upsert(ctx, states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: key,
+ Body: activationData,
+ },
+ })
+ return err
+}
+func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) error {
+ ctx, span := observability.StartSpan("Job Manager", ctx, &map[string]string{
+ "method": "HandleJobEvent",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ scope := model.ReadProperty(event.Metadata, "scope", nil)
+ if scope == "" {
+ scope = "default"
+ }
+
+ if objectType, ok := event.Metadata["objectType"]; ok {
+ var job v1alpha2.JobData
+ var baseUrl string
+ var user string
+ var password string
+ jData, _ := json.Marshal(event.Body)
+ err = json.Unmarshal(jData, &job)
+ if err != nil {
+ return v1alpha2.NewCOAError(nil, "event body is not a job", v1alpha2.BadRequest)
+ }
+
+ err = s.DelayOrSkipJob(ctx, objectType, job)
+ if err != nil {
+ return err
+ }
+
+ baseUrl, err = utils.GetString(s.Manager.Config.Properties, "baseUrl")
+ if err != nil {
+ return err
+ }
+ user, err = utils.GetString(s.Manager.Config.Properties, "user")
+ if err != nil {
+ return err
+ }
+ password, err = utils.GetString(s.Manager.Config.Properties, "password")
+ if err != nil {
+ return err
+ }
+ switch objectType {
+ case "instance":
+ instanceName := job.Id
+ var instance model.InstanceState
+ //get intance
+ instance, err := utils.GetInstance(ctx, baseUrl, instanceName, user, password, scope)
+ if err != nil {
+ return err //TODO: instance is gone
+ }
+
+ if instance.Status == nil {
+ instance.Status = make(map[string]string)
+ }
+
+ //get solution
+ solution, err := utils.GetSolution(ctx, baseUrl, instance.Spec.Solution, user, password, scope)
+ if err != nil {
+ solution = model.SolutionState{
+ Id: instance.Spec.Solution,
+ Spec: &model.SolutionSpec{
+ Components: make([]model.ComponentSpec, 0),
+ },
+ }
+ }
+
+ //get targets
+ var targets []model.TargetState
+ targets, err = utils.GetTargets(ctx, baseUrl, user, password, scope)
+ if err != nil {
+ targets = make([]model.TargetState, 0)
+ }
+
+ //get target candidates
+ targetCandidates := utils.MatchTargets(instance, targets)
+
+ //create deployment spec
+ var deployment model.DeploymentSpec
+ deployment, err = utils.CreateSymphonyDeployment(instance, solution, targetCandidates, nil)
+ if err != nil {
+ return err
+ }
+
+ //call api
+ if job.Action == "UPDATE" {
+ _, err := utils.Reconcile(ctx, baseUrl, user, password, deployment, scope, false)
+ if err != nil {
+ return err
+ } else {
+ s.StateProvider.Upsert(ctx, states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: "i_" + instance.Id,
+ Body: LastSuccessTime{
+ Time: time.Now().UTC(),
+ },
+ },
+ })
+ }
+ }
+ if job.Action == "DELETE" {
+ _, err := utils.Reconcile(ctx, baseUrl, user, password, deployment, scope, true)
+ if err != nil {
+ return err
+ } else {
+ return utils.DeleteInstance(ctx, baseUrl, deployment.Instance.Name, user, password, scope)
+ }
+ }
+ case "target":
+ targetName := job.Id
+ target, err := utils.GetTarget(ctx, baseUrl, targetName, user, password, scope)
+ if err != nil {
+ return err
+ }
+ var deployment model.DeploymentSpec
+ deployment, err = utils.CreateSymphonyDeploymentFromTarget(target)
+ if err != nil {
+ return err
+ }
+ if job.Action == "UPDATE" {
+ _, err := utils.Reconcile(ctx, baseUrl, user, password, deployment, scope, false)
+ if err != nil {
+ return err
+ } else {
+ // TODO: how to handle status updates?
+ s.StateProvider.Upsert(ctx, states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: "t_" + targetName,
+ Body: LastSuccessTime{
+ Time: time.Now().UTC(),
+ },
+ },
+ })
+ }
+ }
+ if job.Action == "DELETE" {
+ _, err := utils.Reconcile(ctx, baseUrl, user, password, deployment, scope, true)
+ if err != nil {
+ return err
+ } else {
+ return utils.DeleteTarget(ctx, baseUrl, targetName, user, password, scope)
+ }
+ }
+ }
+ }
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package reference
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "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/reference"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/reporter"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+ "github.com/oliveagle/jsonpath"
+)
+
+type ReferenceManager struct {
+ managers.Manager
+ ReferenceProviders map[string]reference.IReferenceProvider
+ StateProvider states.IStateProvider
+ Reporter reporter.IReporter
+ CacheLifespan uint64
+}
+
+type CachedItem struct {
+ Created time.Time
+ Item interface{}
+}
+
+func (s *ReferenceManager) 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
+ }
+
+ reportProvider, err := managers.GetReporter(config, providers)
+ if err == nil {
+ s.Reporter = reportProvider
+ } else {
+ return err
+ }
+
+ ctx := contexts.ManagerContext{}
+ err = ctx.Init(context, nil)
+ if err != nil {
+ return err
+ }
+
+ s.CacheLifespan = 60
+ if val, ok := config.Properties["cacheLifespan"]; ok {
+ if i, err := strconv.ParseUint(val, 10, 32); err == nil {
+ s.CacheLifespan = i
+ }
+ }
+
+ s.Context = &ctx
+
+ s.ReferenceProviders = make(map[string]reference.IReferenceProvider)
+
+ for _, p := range providers {
+ if kp, ok := p.(reference.IReferenceProvider); ok {
+ s.ReferenceProviders[kp.ReferenceType()] = kp
+ s.ReferenceProviders[kp.ReferenceType()].SetContext(s.Context)
+ }
+ }
+
+ s.StateProvider.SetContext(s.Context)
+ return nil
+}
+
+func (s *ReferenceManager) GetExt(refType string, namespace string, id1 string, group1 string, kind1 string, version1 string, id2 string, group2 string, kind2 string, version2 string, iteration string, alias string) ([]byte, error) {
+ if group2 != "download" {
+ data1, err := s.Get(refType, id1, namespace, group1, kind1, version1, "", "")
+ if err != nil {
+ return nil, err
+ }
+ data2, err := s.Get(refType, id2, namespace, group2, kind2, version2, "", "")
+ if err != nil {
+ return nil, err
+ }
+ return fillParameters(data1, data2, id1, alias)
+ } else {
+ data1, err := s.Get(refType, id1, namespace, group1, kind1, version1, "", "")
+ if err != nil {
+ return nil, err
+ }
+ obj := make(map[string]interface{}, 0)
+ err = json.Unmarshal(data1, &obj)
+ if err != nil {
+ return nil, err
+ }
+ var specData []byte
+ if v, ok := obj["spec"]; ok {
+ specData, err = json.Marshal(v)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ return nil, v1alpha2.NewCOAError(nil, "resolved object doesn't contain a 'spec' property", v1alpha2.InternalError)
+ }
+
+ model := model.ModelSpec{}
+ err = json.Unmarshal(specData, &model)
+ if err != nil {
+ return nil, err
+ }
+ modelType := safeRead("model.type", model.Properties)
+ if modelType != "customvision" {
+ return nil, v1alpha2.NewCOAError(nil, "only 'customvision' model type is supported", v1alpha2.InternalError)
+ }
+ modelProject := safeRead("model.project", model.Properties)
+ if modelProject == "" {
+ return nil, v1alpha2.NewCOAError(nil, "property 'model.project' is not found", v1alpha2.InternalError)
+ }
+ modelEndpoint := safeRead("model.endpoint", model.Properties)
+ if modelEndpoint == "" {
+ return nil, v1alpha2.NewCOAError(nil, "property 'model.endpoint' is not found", v1alpha2.InternalError)
+ }
+ modelVersions := make(map[string]string)
+ for k, v := range model.Properties {
+ if strings.HasPrefix(k, "model.version.") {
+ modelVersions[k] = v
+ }
+ }
+ if len(modelVersions) == 0 {
+ return nil, v1alpha2.NewCOAError(nil, "no model version are found", v1alpha2.InternalError)
+ }
+ selection := ""
+ if iteration == "latest" {
+ selection = findLatest(modelVersions)
+ } else {
+ if v, ok := modelVersions["model.version."+iteration]; ok {
+ selection = v
+ }
+ }
+ if selection == "" {
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("requested version 'model.version.%s' is not found", iteration), v1alpha2.InternalError)
+ }
+
+ downloadData, err := s.Get("v1alpha2.CustomVision", modelProject, modelEndpoint, kind2, version2, selection, "", "")
+ if err != nil {
+ return nil, err
+ }
+ return downloadData, nil
+ }
+}
+func findLatest(dict map[string]string) string {
+ largest := 0
+ ret := ""
+ for k, v := range dict {
+ vk := k[14:]
+ i, err := strconv.Atoi(vk)
+ if err == nil && i >= largest {
+ largest = i
+ ret = v
+ }
+ }
+ return ret
+}
+
+func safeRead(key string, dict map[string]string) string {
+ if v, ok := dict[key]; ok {
+ return v
+ }
+ return ""
+}
+
+func (s *ReferenceManager) Get(refType string, id string, namespace string, group string, kind string, version string, labelSelector string, fieldSelector string) ([]byte, error) {
+ var entityId string
+ if labelSelector != "" || fieldSelector != "" {
+ entityId = fmt.Sprintf("%s-%s-%s-%s-%s-%s-%s", refType, labelSelector, fieldSelector, namespace, group, kind, version)
+ } else {
+ entityId = fmt.Sprintf("%s-%s-%s-%s-%s-%s", refType, id, namespace, group, kind, version)
+ }
+ entity, err := s.StateProvider.Get(context.TODO(), states.GetRequest{
+ ID: entityId,
+ })
+ if err == nil {
+ data, _ := json.Marshal(entity.Body)
+ cachedItem := CachedItem{}
+ if err == nil {
+ err := json.Unmarshal(data, &cachedItem)
+ if err == nil {
+ if time.Since(cachedItem.Created).Seconds() <= float64(s.CacheLifespan) {
+ cacheData, _ := json.Marshal(cachedItem.Item)
+ return cacheData, nil
+ }
+ }
+ }
+ }
+ var provider reference.IReferenceProvider
+ if p, ok := s.ReferenceProviders[refType]; ok {
+ provider = p
+ } else if len(s.ReferenceProviders) == 1 {
+ for _, v := range s.ReferenceProviders {
+ provider = v
+ break
+ }
+ } else {
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("reference provider for '%s' is not configured", refType), v1alpha2.InternalError)
+ }
+
+ var ref interface{}
+ if labelSelector != "" || fieldSelector != "" {
+ ref, err = provider.List(labelSelector, fieldSelector, namespace, group, kind, version, refType)
+ } else {
+ ref, err = provider.Get(id, namespace, group, kind, version, refType)
+ }
+ if err != nil {
+ return nil, err
+ }
+ s.StateProvider.Upsert(context.Background(), states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: entityId,
+ Body: CachedItem{
+ Created: time.Now(),
+ Item: ref,
+ },
+ },
+ })
+ refData, _ := json.Marshal(ref)
+ return refData, nil
+
+}
+
+func (s *ReferenceManager) Report(id string, namespace string, group string, kind string, version string, properties map[string]string, overwrite bool) error {
+ return s.Reporter.Report(id, namespace, group, kind, version, properties, overwrite)
+}
+func (s *ReferenceManager) Enabled() bool {
+ return s.Config.Properties["poll.enabled"] == "true"
+}
+func (s *ReferenceManager) Poll() []error {
+ return nil
+}
+
+func (s *ReferenceManager) Reconcil() []error {
+ return nil
+}
+
+func fillParameters(data1 []byte, data2 []byte, id string, alias string) ([]byte, error) {
+ params1, err := getParameterMap(data1, "", "") //parameters in skill
+ if err != nil {
+ return nil, err
+ }
+ params2, err := getParameterMap(data2, id, alias) // parameters in instance
+ if err != nil {
+ return nil, err
+ }
+ // for k, _ := range params1 {
+ // key := id + "." + k
+ // if alias != "" {
+ // key = id + "." + alias + "." + k
+ // }
+ // if v2, ok := params2[key]; ok {
+ // params1[k] = v2
+ // }
+ // }
+ for k, _ := range params1 {
+ if v2, ok := params2[k]; ok {
+ params1[k] = v2
+ }
+ }
+ strData := string(data1)
+ for k, v := range params1 {
+ strData = strings.ReplaceAll(strData, "$param("+k+")", v) //TODO: this needs to use property expression syntax instead of string replaces
+ }
+ return []byte(strData), nil
+}
+func getParameterMap(data []byte, skill string, alias string) (map[string]string, error) {
+ var obj interface{}
+ dict := make(map[string]string)
+ err := json.Unmarshal(data, &obj)
+ if err != nil {
+ return nil, err
+ }
+ params, err := jsonpath.JsonPathLookup(obj, "$.parameters")
+ if err == nil {
+ coll := params.(map[string]interface{})
+ for k, p := range coll {
+ dict[k] = p.(string)
+ }
+ }
+ params, err = jsonpath.JsonPathLookup(obj, "$.spec.parameters")
+ if err == nil {
+ coll := params.(map[string]interface{})
+ for k, p := range coll {
+ dict[k] = p.(string)
+ }
+ }
+ if skill != "" && alias != "" {
+ params, err = jsonpath.JsonPathLookup(obj, fmt.Sprintf("$.pipelines[?(@.name == '%s' && @.skill == '%s')].parameters", skill, alias))
+ if err == nil {
+ coll := params.([]interface{})
+ for _, p := range coll {
+ pk := p.(map[string]interface{})
+ for k, v := range pk {
+ dict[k] = v.(string)
+ }
+ }
+ }
+ params, err = jsonpath.JsonPathLookup(obj, fmt.Sprintf("$.spec.pipelines[?(@.name == '%s' && @.skill == '%s')].parameters", skill, alias))
+ if err == nil {
+ coll := params.([]interface{})
+ for _, p := range coll {
+ pk := p.(map[string]interface{})
+ for k, v := range pk {
+ dict[k] = v.(string)
+ }
+ }
+ }
+ }
+
+ return dict, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package sites
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "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/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
+ observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+)
+
+type SitesManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+func (s *SitesManager) 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
+}
+
+// GetCampaign retrieves a CampaignSpec object by name
+func (m *SitesManager) GetSpec(ctx context.Context, name string) (model.SiteState, error) {
+ ctx, span := observability.StartSpan("Sites Manager", ctx, &map[string]string{
+ "method": "GetSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ getRequest := states.GetRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "sites",
+ },
+ }
+ entry, err := m.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return model.SiteState{}, err
+ }
+
+ ret, err := getSiteState(name, entry.Body)
+ if err != nil {
+ return model.SiteState{}, err
+ }
+ return ret, nil
+}
+
+func getSiteState(id string, body interface{}) (model.SiteState, error) {
+ dict := body.(map[string]interface{})
+ spec := dict["spec"]
+ status := dict["status"]
+
+ j, _ := json.Marshal(spec)
+ var rSpec model.SiteSpec
+ err := json.Unmarshal(j, &rSpec)
+ if err != nil {
+ return model.SiteState{}, err
+ }
+
+ var rStatus model.SiteStatus
+
+ if status != nil {
+ j, _ = json.Marshal(status)
+ err = json.Unmarshal(j, &rStatus)
+ if err != nil {
+ return model.SiteState{}, err
+ }
+ }
+ state := model.SiteState{
+ Id: id,
+ Spec: &rSpec,
+ Status: &rStatus,
+ }
+ return state, nil
+}
+
+func (t *SitesManager) ReportState(ctx context.Context, current model.SiteState) error {
+ ctx, span := observability.StartSpan("Sites Manager", ctx, &map[string]string{
+ "method": "ReportState",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ current.Metadata = map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "sites",
+ }
+ getRequest := states.GetRequest{
+ ID: current.Id,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "sites",
+ },
+ }
+
+ entry, err := t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ if !v1alpha2.IsNotFound(err) {
+ return err
+ }
+ err = t.UpsertSpec(ctx, current.Id, *current.Spec)
+ if err != nil {
+ return err
+ }
+ entry, err = t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return err
+ }
+ }
+
+ // This copy is necessary becasue otherwise you could be modifying data in memory stage provider
+ jTransfer, _ := json.Marshal(entry.Body)
+ var dict map[string]interface{}
+ json.Unmarshal(jTransfer, &dict)
+
+ delete(dict, "spec")
+ status := dict["status"]
+
+ j, _ := json.Marshal(status)
+ var rStatus model.SiteStatus
+ err = json.Unmarshal(j, &rStatus)
+ if err != nil {
+ return err
+ }
+ // if current.Status is not nil, update the status using new IsOnline, InstanceStatuses and TargetStatuses
+ // otherwise, only update LastReported as time.Now()
+ if current.Status != nil {
+ rStatus.IsOnline = current.Status.IsOnline
+ rStatus.InstanceStatuses = current.Status.InstanceStatuses
+ rStatus.TargetStatuses = current.Status.TargetStatuses
+ }
+ rStatus.LastReported = time.Now().UTC().Format(time.RFC3339)
+ dict["status"] = rStatus
+
+ entry.Body = dict
+
+ updateRequest := states.UpsertRequest{
+ Value: entry,
+ Metadata: current.Metadata,
+ }
+
+ _, err = t.StateProvider.Upsert(ctx, updateRequest)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *SitesManager) UpsertSpec(ctx context.Context, name string, spec model.SiteSpec) error {
+ ctx, span := observability.StartSpan("Sites Manager", ctx, &map[string]string{
+ "method": "UpsertSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ upsertRequest := states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: name,
+ Body: map[string]interface{}{
+ "apiVersion": model.FederationGroup + "/v1",
+ "kind": "Site",
+ "metadata": map[string]interface{}{
+ "name": name,
+ },
+ "spec": spec,
+ },
+ },
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Site", "metadata": {"name": "${{$site()}}"}}`, model.FederationGroup),
+ "scope": "",
+ "group": model.FederationGroup,
+ "version": "v1",
+ "resource": "sites",
+ },
+ }
+ _, err = m.StateProvider.Upsert(ctx, upsertRequest)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (m *SitesManager) DeleteSpec(ctx context.Context, name string) error {
+ ctx, span := observability.StartSpan("Sites Manager", ctx, &map[string]string{
+ "method": "DeleteSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = m.StateProvider.Delete(ctx, states.DeleteRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "scope": "",
+ "group": model.FederationGroup,
+ "version": "v1",
+ "resource": "sites",
+ },
+ })
+
+ return err
+}
+
+func (t *SitesManager) ListSpec(ctx context.Context) ([]model.SiteState, error) {
+ ctx, span := observability.StartSpan("Sites Manager", ctx, &map[string]string{
+ "method": "ListSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ listRequest := states.ListRequest{
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "sites",
+ },
+ }
+ sites, _, err := t.StateProvider.List(ctx, listRequest)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]model.SiteState, 0)
+ for _, t := range sites {
+ var rt model.SiteState
+ rt, err = getSiteState(t.ID, t.Body)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, rt)
+ }
+ return ret, nil
+}
+func (s *SitesManager) Enabled() bool {
+ return s.VendorContext.SiteInfo.ParentSite.BaseUrl != ""
+}
+func (s *SitesManager) Poll() []error {
+ ctx, span := observability.StartSpan("Sites Manager", context.Background(), &map[string]string{
+ "method": "Poll",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ thisSite, err := s.GetSpec(ctx, s.VendorContext.SiteInfo.SiteId)
+ if err != nil {
+ //TOOD: only ignore not found, and log the error
+ return nil
+ }
+ thisSite.Spec.IsSelf = false
+ jData, _ := json.Marshal(thisSite)
+ utils.UpdateSite(
+ ctx,
+ s.VendorContext.SiteInfo.ParentSite.BaseUrl,
+ s.VendorContext.SiteInfo.SiteId,
+ s.VendorContext.SiteInfo.ParentSite.Username,
+ s.VendorContext.SiteInfo.ParentSite.Password,
+ jData,
+ )
+ return nil
+}
+func (s *SitesManager) Reconcil() []error {
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package solution
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+)
+
+func PlanForDeployment(deployment model.DeploymentSpec, state model.DeploymentState) (model.DeploymentPlan, error) {
+ ret := model.DeploymentPlan{
+ Steps: make([]model.DeploymentStep, 0),
+ }
+ for _, c := range state.Components {
+ for _, t := range state.Targets {
+ key := fmt.Sprintf("%s::%s", c.Name, t.Name) //TODO: this assumes provider/component keys don't contain "::"
+ if v, ok := state.TargetComponent[key]; ok {
+ role := c.Type
+ if role == "" {
+ role = "instance"
+ }
+ action := "update"
+ if strings.HasPrefix(v, "-") {
+ action = "delete"
+ }
+ index := ret.FindLastTargetRole(t.Name, c.Type)
+ if index < 0 || !ret.CanAppendToStep(index, c) {
+ ret.Steps = append(ret.Steps, model.DeploymentStep{
+ Target: t.Name,
+ Role: role,
+ IsFirst: index < 0,
+ Components: []model.ComponentStep{
+ {
+ Action: action,
+ Component: c,
+ },
+ },
+ })
+ } else {
+ ret.Steps[index].Components = append(ret.Steps[index].Components, model.ComponentStep{
+ Action: action,
+ Component: c,
+ })
+ }
+ }
+ }
+ }
+ return ret.RevisedForDeletion(), nil
+}
+
+func NewDeploymentState(deployment model.DeploymentSpec) (model.DeploymentState, error) {
+ ret := model.DeploymentState{
+ Components: make([]model.ComponentSpec, 0),
+ Targets: make([]model.TargetDesc, 0),
+ TargetComponent: make(map[string]string),
+ }
+
+ components, err := sortByDepedencies(deployment.Solution.Components)
+ if err != nil {
+ return ret, err
+ }
+
+ for _, component := range components {
+ ret.Components = append(ret.Components, component)
+
+ providers := findComponentProviders(component.Name, deployment)
+ for k, v := range providers {
+ found := false
+ for _, t := range ret.Targets {
+ if t.Name == k {
+ found = true
+ break
+ }
+ }
+ if !found {
+ ret.Targets = append(ret.Targets, model.TargetDesc{Name: k, Spec: v})
+ }
+ t := component.Type
+ if t == "" {
+ t = "instance"
+ }
+ ret.TargetComponent[fmt.Sprintf("%s::%s", component.Name, k)] = t //TODO: this assumes provider/component keys don't contain "::"
+ }
+ }
+
+ sort.Sort(model.ByTargetName(ret.Targets)) //sort target by name for easier testing
+
+ return ret, nil
+}
+func MergeDeploymentStates(previous *model.DeploymentState, current model.DeploymentState) model.DeploymentState {
+ if previous == nil {
+ return current
+ }
+ // merge components
+ for _, c := range previous.Components {
+ found := false
+ for _, cc := range current.Components {
+ if cc.Name == c.Name {
+ found = true
+ break
+ }
+ }
+ if !found {
+ current.Components = append(current.Components, c)
+ }
+ }
+ // merge targets
+ for _, t := range previous.Targets {
+ found := false
+ for _, tt := range current.Targets {
+ if tt.Name == t.Name {
+ found = true
+ break
+ }
+ }
+ if !found {
+ current.Targets = append(current.Targets, t)
+ }
+ }
+ // merge state matrix
+ for k, v := range previous.TargetComponent {
+ if _, ok := current.TargetComponent[k]; !ok {
+ if !strings.HasPrefix(v, "-") {
+ current.TargetComponent[k] = "-" + v
+ }
+ }
+ }
+ return current
+}
+func findComponentProviders(component string, deployment model.DeploymentSpec) map[string]model.TargetSpec {
+ ret := make(map[string]model.TargetSpec)
+ for k, v := range deployment.Assignments {
+ if v != "" {
+ if strings.Contains(v, "{"+component+"}") {
+ if t, ok := deployment.Targets[k]; ok {
+ ret[k] = t
+ }
+ }
+ }
+ }
+ return ret
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package solution
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ sp "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers"
+ tgt "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target"
+ api_utils "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/contexts"
+ "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"
+ config "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/config"
+ secret "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/secret"
+ states "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var log = logger.NewLogger("coa.runtime")
+var lock sync.Mutex
+
+const (
+ SYMPHONY_AGENT string = "/symphony-agent:"
+ ENV_NAME string = "SYMPHONY_AGENT_ADDRESS"
+)
+
+type SolutionManager struct {
+ managers.Manager
+ TargetProviders map[string]tgt.ITargetProvider
+ StateProvider states.IStateProvider
+ ConfigProvider config.IExtConfigProvider
+ SecretProvoider secret.ISecretProvider
+}
+
+type SolutionManagerDeploymentState struct {
+ Spec model.DeploymentSpec `json:"spec,omitempty"`
+ State model.DeploymentState `json:"state,omitempty"`
+}
+
+func (s *SolutionManager) 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
+ }
+ s.TargetProviders = make(map[string]tgt.ITargetProvider)
+ for k, v := range providers {
+ if p, ok := v.(tgt.ITargetProvider); ok {
+ s.TargetProviders[k] = p
+ }
+ }
+
+ stateprovider, err := managers.GetStateProvider(config, providers)
+ if err == nil {
+ s.StateProvider = stateprovider
+ } else {
+ return err
+ }
+
+ configProvider, err := managers.GetExtConfigProvider(config, providers)
+ if err == nil {
+ s.ConfigProvider = configProvider
+ } else {
+ return err
+ }
+
+ secretProvider, err := managers.GetSecretProvider(config, providers)
+ if err == nil {
+ s.SecretProvoider = secretProvider
+ } else {
+ return err
+ }
+
+ return nil
+}
+
+func (s *SolutionManager) getPreviousState(ctx context.Context, instance string, scope string) *SolutionManagerDeploymentState {
+ state, err := s.StateProvider.Get(ctx, states.GetRequest{
+ ID: instance,
+ Metadata: map[string]string{
+ "scope": scope,
+ },
+ })
+ if err == nil {
+ var managerState SolutionManagerDeploymentState
+ jData, _ := json.Marshal(state.Body)
+ err = json.Unmarshal(jData, &managerState)
+ if err == nil {
+ return &managerState
+ }
+ return nil
+ }
+ return nil
+}
+func (s *SolutionManager) GetSummary(ctx context.Context, key string, scope string) (model.SummaryResult, error) {
+ // lock.Lock()
+ // defer lock.Unlock()
+
+ iCtx, span := observability.StartSpan("Solution Manager", ctx, &map[string]string{
+ "method": "GetSummary",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ log.Info(" M (Solution): get summary")
+
+ state, err := s.StateProvider.Get(iCtx, states.GetRequest{
+ ID: fmt.Sprintf("%s-%s", "summary", key),
+ Metadata: map[string]string{
+ "scope": scope,
+ },
+ })
+ if err != nil {
+ log.Errorf(" M (Solution): failed to get deployment summary[%s]: %+v", key, err)
+ return model.SummaryResult{}, err
+ }
+
+ var result model.SummaryResult
+ jData, _ := json.Marshal(state.Body)
+ err = json.Unmarshal(jData, &result)
+ if err != nil {
+ log.Errorf(" M (Solution): failed to deserailze deployment summary[%s]: %+v", key, err)
+ return model.SummaryResult{}, err
+ }
+
+ return result, nil
+}
+
+func (s *SolutionManager) sendHeartbeat(id string, remove bool, stopCh chan struct{}) {
+ ticker := time.NewTicker(30 * time.Second)
+ defer ticker.Stop()
+
+ action := "update"
+ if remove {
+ action = "delete"
+ }
+
+ for {
+ select {
+ case <-ticker.C:
+ s.VendorContext.Publish("heartbeat", v1alpha2.Event{
+ Body: v1alpha2.HeartBeatData{
+ JobId: id,
+ Action: action,
+ Time: time.Now().UTC(),
+ },
+ })
+ case <-stopCh:
+ return // Exit the goroutine when the stop signal is received
+ }
+ }
+}
+
+func (s *SolutionManager) Reconcile(ctx context.Context, deployment model.DeploymentSpec, remove bool, scope string) (model.SummarySpec, error) {
+ lock.Lock()
+ defer lock.Unlock()
+
+ stopCh := make(chan struct{})
+ defer close(stopCh)
+ go s.sendHeartbeat(deployment.Instance.Name, remove, stopCh)
+
+ iCtx, span := observability.StartSpan("Solution Manager", ctx, &map[string]string{
+ "method": "Reconcile",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ log.Info(" M (Solution): reconciling")
+
+ summary := model.SummarySpec{
+ TargetResults: make(map[string]model.TargetResultSpec),
+ TargetCount: len(deployment.Targets),
+ SuccessCount: 0,
+ }
+
+ if s.VendorContext != nil && s.VendorContext.EvaluationContext != nil {
+ context := s.VendorContext.EvaluationContext.Clone()
+ context.DeploymentSpec = deployment
+ context.Value = deployment
+ context.Component = ""
+ deployment, err = api_utils.EvaluateDeployment(*context)
+ }
+
+ if err != nil {
+ if remove {
+ log.Infof(" M (Solution): skipped failure to evaluate deployment spec: %+v", err)
+ } else {
+ summary.SummaryMessage = "failed to evaluate deployment spec: " + err.Error()
+ log.Errorf(" M (Solution): failed to evaluate deployment spec: %+v", err)
+ s.saveSummary(iCtx, deployment, summary, scope)
+ return summary, err
+ }
+ }
+
+ previousDesiredState := s.getPreviousState(iCtx, deployment.Instance.Name, scope)
+ currentDesiredState, err := NewDeploymentState(deployment)
+ if err != nil {
+ summary.SummaryMessage = "failed to create target manager state from deployment spec: " + err.Error()
+ log.Errorf(" M (Solution): failed to create target manager state from deployment spec: %+v", err)
+ s.saveSummary(iCtx, deployment, summary, scope)
+ return summary, err
+ }
+ currentState, _, err := s.Get(iCtx, deployment)
+ if err != nil {
+ summary.SummaryMessage = "failed to get current state: " + err.Error()
+ log.Errorf(" M (Solution): failed to get current state: %+v", err)
+ s.saveSummary(iCtx, deployment, summary, scope)
+ return summary, err
+ }
+
+ desiredState := currentDesiredState
+ if previousDesiredState != nil {
+ desiredState = MergeDeploymentStates(&previousDesiredState.State, currentDesiredState)
+ }
+
+ if remove {
+ desiredState.MarkRemoveAll()
+ }
+
+ mergedState := MergeDeploymentStates(¤tState, desiredState)
+
+ plan, err := PlanForDeployment(deployment, mergedState)
+ if err != nil {
+ summary.SummaryMessage = "failed to plan for deployment: " + err.Error()
+ log.Errorf(" M (Solution): failed to plan for deployment: %+v", err)
+ s.saveSummary(iCtx, deployment, summary, scope)
+ return summary, err
+ }
+
+ col := api_utils.MergeCollection(deployment.Solution.Metadata, deployment.Instance.Metadata)
+ dep := deployment
+ dep.Instance.Metadata = col
+ someStepsRan := false
+
+ for _, step := range plan.Steps {
+ dep.ActiveTarget = step.Target
+ agent := findAgent(deployment.Targets[step.Target])
+ if agent != "" {
+ col[ENV_NAME] = agent
+ } else {
+ delete(col, ENV_NAME)
+ }
+ var override tgt.ITargetProvider
+ if v, ok := s.TargetProviders[step.Target]; ok {
+ override = v
+ }
+ var provider providers.IProvider
+ provider, err = sp.CreateProviderForTargetRole(s.Context, step.Role, deployment.Targets[step.Target], override)
+ if err != nil {
+ summary.SummaryMessage = "failed to create provider:" + err.Error()
+ log.Errorf(" M (Solution): failed to create provider: %+v", err)
+ s.saveSummary(ctx, deployment, summary, scope)
+ return summary, err
+ }
+
+ if previousDesiredState != nil {
+ testState := MergeDeploymentStates(&previousDesiredState.State, currentState)
+ if s.canSkipStep(iCtx, step, step.Target, provider.(tgt.ITargetProvider), previousDesiredState.State.Components, testState) {
+ continue
+ }
+ }
+ someStepsRan = true
+ retryCount := 1
+ //TODO: set to 1 for now. Although retrying can help to handle transient errors, in more cases
+ // an error condition can't be resolved quickly.
+ var stepError error
+ var componentResults map[string]model.ComponentResultSpec
+
+ // for _, component := range step.Components {
+ // for k, v := range component.Component.Properties {
+ // if strV, ok := v.(string); ok {
+ // parser := api_utils.NewParser(strV)
+ // eCtx := s.VendorContext.EvaluationContext.Clone()
+ // eCtx.DeploymentSpec = deployment
+ // eCtx.Component = component.Component.Name
+ // val, err := parser.Eval(*eCtx)
+ // if err == nil {
+ // component.Component.Properties[k] = val
+ // } else {
+ // log.Errorf(" M (Solution): failed to evaluate property: %+v", err)
+ // summary.SummaryMessage = fmt.Sprintf("failed to evaluate property '%s' on component '%s: %s", k, component.Component.Name, err.Error())
+ // s.saveSummary(ctx, deployment, summary)
+ // observ_utils.CloseSpanWithError(span, &err)
+ // return summary, err
+ // }
+ // }
+ // }
+ // }
+
+ for i := 0; i < retryCount; i++ {
+ componentResults, stepError = (provider.(tgt.ITargetProvider)).Apply(iCtx, dep, step, false)
+ if stepError == nil {
+ summary.UpdateTargetResult(step.Target, model.TargetResultSpec{Status: "OK", Message: "", ComponentResults: componentResults})
+ break
+ } else {
+ summary.UpdateTargetResult(step.Target, model.TargetResultSpec{Status: "Error", Message: stepError.Error(), ComponentResults: componentResults}) // TODO: this keeps only the last error on the target
+ time.Sleep(5 * time.Second) //TODO: make this configurable?
+ }
+ }
+ if stepError != nil {
+ log.Errorf(" M (Solution): failed to execute deployment step: %+v", stepError)
+ s.saveSummary(iCtx, deployment, summary, scope)
+ err = stepError
+ return summary, err
+ }
+ }
+
+ mergedState.ClearAllRemoved()
+
+ // TODO: delete the state if the mergedState is empty (doesn't have any ComponentTarget assignements)
+ s.StateProvider.Upsert(iCtx, states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: deployment.Instance.Name,
+ Body: SolutionManagerDeploymentState{
+ Spec: deployment,
+ State: mergedState,
+ },
+ },
+ Metadata: map[string]string{
+ "scope": scope,
+ },
+ })
+
+ summary.Skipped = !someStepsRan
+ if summary.Skipped {
+ summary.SuccessCount = summary.TargetCount
+ }
+ summary.IsRemoval = remove
+ s.saveSummary(iCtx, deployment, summary, scope)
+ return summary, nil
+}
+func (s *SolutionManager) saveSummary(ctx context.Context, deployment model.DeploymentSpec, summary model.SummarySpec, scope string) {
+ // TODO: delete this state when time expires. This should probably be invoked by the vendor (via GetSummary method, for instance)
+ s.StateProvider.Upsert(ctx, states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: fmt.Sprintf("%s-%s", "summary", deployment.Instance.Name),
+ Body: model.SummaryResult{
+ Summary: summary,
+ Generation: deployment.Generation,
+ Time: time.Now().UTC(),
+ },
+ },
+ Metadata: map[string]string{
+ "scope": scope,
+ },
+ })
+}
+func (s *SolutionManager) canSkipStep(ctx context.Context, step model.DeploymentStep, target string, provider tgt.ITargetProvider, currentComponents []model.ComponentSpec, state model.DeploymentState) bool {
+
+ for _, newCom := range step.Components {
+ key := fmt.Sprintf("%s::%s", newCom.Component.Name, target)
+ if newCom.Action == "delete" {
+ for _, c := range currentComponents {
+ if c.Name == newCom.Component.Name && state.TargetComponent[key] != "" {
+ return false // current component still exists, desired is to remove it. The step can't be skipped
+ }
+ }
+
+ } else {
+ found := false
+ for _, c := range currentComponents {
+ if c.Name == newCom.Component.Name && state.TargetComponent[key] != "" && !strings.HasPrefix(state.TargetComponent[key], "-") {
+ found = true
+ rule := provider.GetValidationRule(ctx)
+ if rule.IsComponentChanged(c, newCom.Component) {
+ return false // component has changed, can't skip the step
+ }
+ break
+ }
+ }
+ if !found {
+ return false //current component doesn't exist, desired is to update it. The step can't be skipped
+ }
+ }
+ }
+ return true
+}
+func (s *SolutionManager) Get(ctx context.Context, deployment model.DeploymentSpec) (model.DeploymentState, []model.ComponentSpec, error) {
+ iCtx, span := observability.StartSpan("Solution Manager", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+ log.Info(" M (Solution): getting deployment")
+
+ ret := model.DeploymentState{}
+
+ state, err := NewDeploymentState(deployment)
+ if err != nil {
+ log.Errorf(" M (Solution): failed to create manager state for deployment: %+v", err)
+ return ret, nil, err
+ }
+ plan, err := PlanForDeployment(deployment, state)
+ if err != nil {
+ log.Errorf(" M (Solution): failed to plan for deployment: %+v", err)
+ return ret, nil, err
+ }
+ ret = state
+ ret.TargetComponent = make(map[string]string)
+ retComponents := make([]model.ComponentSpec, 0)
+ for _, step := range plan.Steps {
+ var override tgt.ITargetProvider
+ if v, ok := s.TargetProviders[step.Target]; ok {
+ override = v
+ }
+ var provider providers.IProvider
+ provider, err = sp.CreateProviderForTargetRole(s.Context, step.Role, deployment.Targets[step.Target], override)
+ if err != nil {
+ log.Errorf(" M (Solution): failed to create provider: %+v", err)
+ return ret, nil, err
+ }
+ var components []model.ComponentSpec
+ components, err = (provider.(tgt.ITargetProvider)).Get(iCtx, deployment, step.Components)
+ if err != nil {
+ log.Errorf(" M (Solution): failed to get: %+v", err)
+ return ret, nil, err
+ }
+ for _, c := range components {
+ key := fmt.Sprintf("%s::%s", c.Name, step.Target)
+ role := c.Type
+ if role == "" {
+ role = "container"
+ }
+ ret.TargetComponent[key] = role
+ found := false
+ for _, rc := range retComponents {
+ if rc.Name == c.Name {
+ found = true
+ break
+ }
+ }
+ if !found {
+ retComponents = append(retComponents, c)
+ }
+ }
+ }
+ return ret, retComponents, nil
+}
+func (s *SolutionManager) Enabled() bool {
+ return false
+}
+func (s *SolutionManager) Poll() []error {
+ return nil
+}
+func (s *SolutionManager) Reconcil() []error {
+ return nil
+}
+func findAgent(target model.TargetSpec) string {
+ for _, c := range target.Components {
+ if v, ok := c.Properties[model.ContainerImage]; ok {
+ if strings.Contains(fmt.Sprintf("%v", v), SYMPHONY_AGENT) {
+ return c.Name
+ }
+ }
+ }
+ return ""
+}
+func sortByDepedencies(components []model.ComponentSpec) ([]model.ComponentSpec, error) {
+ size := len(components)
+ inDegrees := make([]int, size)
+ queue := make([]int, 0)
+ for i, c := range components {
+ inDegrees[i] = len(c.Dependencies)
+ if inDegrees[i] == 0 {
+ queue = append(queue, i)
+ }
+ }
+ ret := make([]model.ComponentSpec, 0)
+ for len(queue) > 0 {
+ ret = append(ret, components[queue[0]])
+ queue = queue[1:]
+ for i, c := range components {
+ found := false
+ for _, d := range c.Dependencies {
+ if d == ret[len(ret)-1].Name {
+ found = true
+ break
+ }
+ }
+ if found {
+ inDegrees[i] -= 1
+ if inDegrees[i] == 0 {
+ queue = append(queue, i)
+ }
+ }
+ }
+ }
+ if len(ret) != size {
+ return nil, errors.New("circular dependencies or unresolved dependencies detected in components")
+ }
+ return ret, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package solutions
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+
+ observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+)
+
+type SolutionsManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+func (s *SolutionsManager) 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 *SolutionsManager) DeleteSpec(ctx context.Context, name string, scope string) error {
+ ctx, span := observability.StartSpan("Solutions Manager", ctx, &map[string]string{
+ "method": "DeleteSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = t.StateProvider.Delete(ctx, states.DeleteRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "scope": scope,
+ "group": model.SolutionGroup,
+ "version": "v1",
+ "resource": "solutions",
+ },
+ })
+ return err
+}
+
+func (t *SolutionsManager) UpsertSpec(ctx context.Context, name string, spec model.SolutionSpec, scope string) error {
+ ctx, span := observability.StartSpan("Solutions Manager", ctx, &map[string]string{
+ "method": "UpsertSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ upsertRequest := states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: name,
+ Body: map[string]interface{}{
+ "apiVersion": model.SolutionGroup + "/v1",
+ "kind": "Solution",
+ "metadata": map[string]interface{}{
+ "name": name,
+ },
+ "spec": spec,
+ },
+ },
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Solution", "metadata": {"name": "${{$solution()}}"}}`, model.SolutionGroup),
+ "scope": scope,
+ "group": model.SolutionGroup,
+ "version": "v1",
+ "resource": "solutions",
+ },
+ }
+ _, err = t.StateProvider.Upsert(ctx, upsertRequest)
+ return err
+}
+
+func (t *SolutionsManager) ListSpec(ctx context.Context, scope string) ([]model.SolutionState, error) {
+ ctx, span := observability.StartSpan("Solutions Manager", ctx, &map[string]string{
+ "method": "ListSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ listRequest := states.ListRequest{
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.SolutionGroup,
+ "resource": "solutions",
+ "scope": scope,
+ },
+ }
+ solutions, _, err := t.StateProvider.List(ctx, listRequest)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]model.SolutionState, 0)
+ for _, t := range solutions {
+ var rt model.SolutionState
+ rt, err = getSolutionState(t.ID, t.Body)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, rt)
+ }
+ return ret, nil
+}
+
+func getSolutionState(id string, body interface{}) (model.SolutionState, error) {
+ dict := body.(map[string]interface{})
+ spec := dict["spec"]
+
+ j, _ := json.Marshal(spec)
+ var rSpec model.SolutionSpec
+ err := json.Unmarshal(j, &rSpec)
+ if err != nil {
+ return model.SolutionState{}, err
+ }
+ scope, exist := dict["scope"]
+ var s string
+ if !exist {
+ s = "default"
+ } else {
+ s = scope.(string)
+ }
+
+ state := model.SolutionState{
+ Id: id,
+ Scope: s,
+ Spec: &rSpec,
+ }
+ return state, nil
+}
+
+func (t *SolutionsManager) GetSpec(ctx context.Context, id string, scope string) (model.SolutionState, error) {
+ ctx, span := observability.StartSpan("Solutions Manager", ctx, &map[string]string{
+ "method": "GetSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ getRequest := states.GetRequest{
+ ID: id,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.SolutionGroup,
+ "resource": "solutions",
+ "scope": scope,
+ },
+ }
+ target, err := t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return model.SolutionState{}, err
+ }
+
+ ret, err := getSolutionState(id, target.Body)
+ if err != nil {
+ return model.SolutionState{}, err
+ }
+ return ret, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package stage
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "reflect"
+ "strconv"
+ "sync"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ symproviders "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/remote"
+ "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/contexts"
+ "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/states"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var log = logger.NewLogger("coa.runtime")
+
+type StageManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+type TaskResult struct {
+ Outputs map[string]interface{}
+ Site string
+ Error error
+}
+
+func (t *TaskResult) GetError() error {
+ if t.Error != nil {
+ return t.Error
+ }
+ if v, ok := t.Outputs["__status"]; ok {
+ switch sv := v.(type) {
+ case v1alpha2.State:
+ break
+ case int:
+ state := v1alpha2.State(sv)
+ stateValue := reflect.ValueOf(state)
+ if stateValue.Type() != reflect.TypeOf(v1alpha2.State(0)) {
+ return fmt.Errorf("invalid state %d", sv)
+ }
+ t.Outputs["__status"] = state
+ case string:
+ vInt, err := strconv.ParseInt(sv, 10, 32)
+ if err != nil {
+ return fmt.Errorf("invalid state %s", sv)
+ }
+ state := v1alpha2.State(vInt)
+ stateValue := reflect.ValueOf(state)
+ if stateValue.Type() != reflect.TypeOf(v1alpha2.State(0)) {
+ return fmt.Errorf("invalid state %d", vInt)
+ }
+ t.Outputs["__status"] = state
+ default:
+ return fmt.Errorf("invalid state %v", v)
+ }
+
+ if t.Outputs["__status"] != v1alpha2.OK {
+ if v, ok := t.Outputs["__error"]; ok {
+ return v1alpha2.NewCOAError(nil, v.(string), t.Outputs["__status"].(v1alpha2.State))
+ } else {
+ return fmt.Errorf("stage returned unsuccessful status without an error")
+ }
+ }
+ }
+ return nil
+}
+
+type PendingTask struct {
+ Sites []string `json:"sites"`
+ OutputContext map[string]map[string]interface{} `json:"outputContext,omitempty"`
+}
+
+func (s *StageManager) 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 (s *StageManager) Enabled() bool {
+ return s.Config.Properties["poll.enabled"] == "true"
+}
+func (s *StageManager) Poll() []error {
+ return nil
+}
+func (s *StageManager) Reconcil() []error {
+ return nil
+}
+func (s *StageManager) ResumeStage(status model.ActivationStatus, cam model.CampaignSpec) (*v1alpha2.ActivationData, error) {
+ log.Debugf(" M (Stage): ResumeStage: %v\n", status)
+ campaign := status.Outputs["__campaign"].(string)
+ activation := status.Outputs["__activation"].(string)
+ activationGeneration := status.Outputs["__activationGeneration"].(string)
+ site := status.Outputs["__site"].(string)
+ stage := status.Outputs["__stage"].(string)
+
+ entry, err := s.StateProvider.Get(context.TODO(), states.GetRequest{
+ ID: fmt.Sprintf("%s-%s-%s", campaign, activation, activationGeneration),
+ })
+ if err != nil {
+ return nil, err
+ }
+ jData, _ := json.Marshal(entry.Body)
+ var p PendingTask
+ err = json.Unmarshal(jData, &p)
+ if err == nil {
+ // //find site in p.Sites
+ // found := false
+ // for _, s := range p.Sites {
+ // if s == site {
+ // found = true
+ // break
+ // }
+ // }
+ // if !found {
+ // return nil, fmt.Errorf("site %s is not found in pending task", site)
+ // }
+ //remove site from p.Sites
+ newSites := make([]string, 0)
+ for _, s := range p.Sites {
+ if s != site {
+ newSites = append(newSites, s)
+ }
+ }
+ if len(newSites) == 0 {
+ err := s.StateProvider.Delete(context.TODO(), states.DeleteRequest{
+ ID: fmt.Sprintf("%s-%s-%s", campaign, activation, activationGeneration),
+ })
+ if err != nil {
+ return nil, err
+ }
+ //find the next stage
+ if cam.SelfDriving {
+ outputs := p.OutputContext
+ if outputs == nil {
+ outputs = make(map[string]map[string]interface{})
+ }
+ outputs[stage] = status.Outputs
+ nextStage := ""
+ if currentStage, ok := cam.Stages[stage]; ok {
+ parser := utils.NewParser(currentStage.StageSelector)
+
+ eCtx := s.VendorContext.EvaluationContext.Clone()
+ eCtx.Inputs = status.Inputs
+ log.Debugf(" M (Stage): ResumeStage evaluation inputs: %v", eCtx.Inputs)
+ if eCtx.Inputs != nil {
+ if v, ok := eCtx.Inputs["context"]; ok {
+ eCtx.Value = v
+ }
+ }
+ eCtx.Outputs = outputs
+ val, err := parser.Eval(*eCtx)
+ if err != nil {
+ return nil, err
+ }
+ sVal := ""
+ if val != nil {
+ sVal = val.(string)
+ }
+ if sVal != "" {
+ if _, ok := cam.Stages[sVal]; ok {
+ nextStage = sVal
+ } else {
+ return nil, fmt.Errorf("stage %s is not found", sVal)
+ }
+ }
+ }
+ if nextStage != "" {
+ activationData := &v1alpha2.ActivationData{
+ Campaign: campaign,
+ Activation: activation,
+ ActivationGeneration: activationGeneration,
+ Stage: nextStage,
+ Inputs: status.Inputs,
+ Provider: cam.Stages[nextStage].Provider,
+ Config: cam.Stages[nextStage].Config,
+ Outputs: outputs,
+ TriggeringStage: stage,
+ Schedule: cam.Stages[nextStage].Schedule,
+ }
+ log.Debugf(" M (Stage): Activating next stage: %s\n", activationData.Stage)
+ return activationData, nil
+ } else {
+ log.Debugf(" M (Stage): No next stage found\n")
+ return nil, nil
+ }
+ }
+ return nil, nil
+ } else {
+ p.Sites = newSites
+ _, err := s.StateProvider.Upsert(context.TODO(), states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: fmt.Sprintf("%s-%s-%s", campaign, activation, activationGeneration),
+ Body: p,
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+ } else {
+ return nil, fmt.Errorf("invalid pending task")
+ }
+
+ return nil, nil
+}
+func (s *StageManager) HandleDirectTriggerEvent(ctx context.Context, triggerData v1alpha2.ActivationData) model.ActivationStatus {
+ ctx, span := observability.StartSpan("Stage Manager", ctx, &map[string]string{
+ "method": "HandleDirectTriggerEvent",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ status := model.ActivationStatus{
+ Stage: "",
+ NextStage: "",
+ Outputs: map[string]interface{}{},
+ Status: v1alpha2.Untouched,
+ ErrorMessage: "",
+ IsActive: true,
+ }
+ factory := symproviders.SymphonyProviderFactory{}
+ provider, err := factory.CreateProvider(triggerData.Provider, triggerData.Config)
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ return status
+ }
+
+ if _, ok := provider.(contexts.IWithManagerContext); ok {
+ provider.(contexts.IWithManagerContext).SetContext(s.Manager.Context)
+ } else {
+ log.Errorf(" M (Stage): provider %s does not implement IWithManagerContext", triggerData.Provider)
+ }
+
+ isRemote := false
+ if _, ok := provider.(*remote.RemoteStageProvider); ok {
+ isRemote = true
+ provider.(*remote.RemoteStageProvider).SetOutputsContext(triggerData.Outputs)
+ }
+
+ if triggerData.Schedule != nil && !isRemote {
+ s.Context.Publish("schedule", v1alpha2.Event{
+ Body: triggerData,
+ })
+ status.Outputs["__status"] = v1alpha2.Delayed
+ status.Status = v1alpha2.Paused
+ status.IsActive = false
+ return status
+ }
+
+ outputs, _, err := provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, triggerData.Inputs)
+
+ result := TaskResult{
+ Outputs: outputs,
+ Error: err,
+ Site: s.VendorContext.SiteInfo.SiteId,
+ }
+
+ err = result.GetError()
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ status.Outputs = carryOutPutsToErrorStatus(outputs, err, "")
+ result.Outputs = carryOutPutsToErrorStatus(outputs, err, "")
+ return status
+ }
+ status.Outputs = outputs
+ status.Outputs["__status"] = v1alpha2.OK
+ status.Outputs["__campaign"] = triggerData.Campaign
+ status.Outputs["__activation"] = triggerData.Activation
+ status.Outputs["__activationGeneration"] = triggerData.ActivationGeneration
+ status.Outputs["__stage"] = triggerData.Stage
+ status.Outputs["__site"] = s.VendorContext.SiteInfo.SiteId
+ status.Status = v1alpha2.Done
+ status.IsActive = false
+ return status
+}
+func carryOutPutsToErrorStatus(outputs map[string]interface{}, err error, site string) map[string]interface{} {
+ ret := make(map[string]interface{})
+ statusKey := "__status"
+ if site != "" {
+ statusKey = fmt.Sprintf("%s.%s", statusKey, site)
+ }
+ errorKey := "__error"
+ if site != "" {
+ errorKey = fmt.Sprintf("%s.%s", errorKey, site)
+ }
+ for k, v := range outputs {
+ ret[k] = v
+ }
+ if _, ok := ret[statusKey]; !ok {
+ if cErr, ok := err.(v1alpha2.COAError); ok {
+ ret[statusKey] = cErr.State
+ } else {
+ ret[statusKey] = v1alpha2.InternalError
+ }
+ }
+ if _, ok := ret[errorKey]; !ok {
+ ret[errorKey] = err.Error()
+ }
+ return ret
+}
+func (s *StageManager) HandleTriggerEvent(ctx context.Context, campaign model.CampaignSpec, triggerData v1alpha2.ActivationData) (model.ActivationStatus, *v1alpha2.ActivationData) {
+ ctx, span := observability.StartSpan("Stage Manager", ctx, &map[string]string{
+ "method": "HandleTriggerEvent",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ log.Info(" M (Stage): HandleTriggerEvent")
+ status := model.ActivationStatus{
+ Stage: triggerData.Stage,
+ NextStage: "",
+ Outputs: nil,
+ Status: v1alpha2.Untouched,
+ ErrorMessage: "",
+ IsActive: true,
+ }
+ var activationData *v1alpha2.ActivationData
+ if currentStage, ok := campaign.Stages[triggerData.Stage]; ok {
+ sites := make([]string, 0)
+ if currentStage.Contexts != "" {
+ parser := utils.NewParser(currentStage.Contexts)
+
+ eCtx := s.VendorContext.EvaluationContext.Clone()
+ eCtx.Inputs = triggerData.Inputs
+ log.Debugf(" M (Stage): HandleTriggerEvent evaluation inputs 1: %v", eCtx.Inputs)
+ if eCtx.Inputs != nil {
+ if v, ok := eCtx.Inputs["context"]; ok {
+ eCtx.Value = v
+ }
+ }
+ eCtx.Outputs = triggerData.Outputs
+ var val interface{}
+ val, err = parser.Eval(*eCtx)
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to evaluate context: %v", err)
+ return status, activationData
+ }
+ if _, ok := val.([]string); ok {
+ sites = val.([]string)
+ } else if _, ok := val.([]interface{}); ok {
+ for _, v := range val.([]interface{}) {
+ sites = append(sites, v.(string))
+ }
+ } else if _, ok := val.(string); ok {
+ sites = append(sites, val.(string))
+ } else {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = fmt.Sprintf("invalid context %s", currentStage.Contexts)
+ status.IsActive = false
+ log.Errorf(" M (Stage): invalid context: %v", currentStage.Contexts)
+ return status, activationData
+ }
+ } else {
+ sites = append(sites, s.VendorContext.SiteInfo.SiteId)
+ }
+
+ inputs := triggerData.Inputs
+ if inputs == nil {
+ inputs = make(map[string]interface{})
+ }
+
+ if currentStage.Inputs != nil {
+ for k, v := range currentStage.Inputs {
+ inputs[k] = v
+ }
+ }
+
+ log.Debugf(" M (Stage): HandleTriggerEvent before evaluation inputs 2: %v", inputs)
+
+ // inject default inputs
+ inputs["__campaign"] = triggerData.Campaign
+ inputs["__activation"] = triggerData.Activation
+ inputs["__stage"] = triggerData.Stage
+ inputs["__activationGeneration"] = triggerData.ActivationGeneration
+ inputs["__previousStage"] = triggerData.TriggeringStage
+ inputs["__site"] = s.VendorContext.SiteInfo.SiteId
+ if triggerData.Schedule != nil {
+ jSchedule, _ := json.Marshal(triggerData.Schedule)
+ inputs["__schedule"] = string(jSchedule)
+ }
+ for k, v := range inputs {
+ var val interface{}
+ val, err = s.traceValue(v, inputs, triggerData.Outputs)
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to evaluate input: %v", err)
+ return status, activationData
+ }
+ inputs[k] = val
+ }
+
+ if triggerData.Outputs != nil {
+ if v, ok := triggerData.Outputs[triggerData.Stage]; ok {
+ if vs, ok := v["__state"]; ok {
+ inputs["__state"] = vs
+ }
+ }
+ }
+
+ log.Debugf(" M (Stage): HandleTriggerEvent after evaluation inputs 2: %v", inputs)
+
+ factory := symproviders.SymphonyProviderFactory{}
+ var provider providers.IProvider
+ provider, err = factory.CreateProvider(triggerData.Provider, triggerData.Config)
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to create provider: %v", err)
+ return status, activationData
+ }
+
+ if _, ok := provider.(contexts.IWithManagerContext); ok {
+ provider.(contexts.IWithManagerContext).SetContext(s.Manager.Context)
+ } else {
+ log.Errorf(" M (Stage): provider %s does not implement IWithManagerContext", triggerData.Provider)
+ }
+
+ numTasks := len(sites)
+ waitGroup := sync.WaitGroup{}
+ results := make(chan TaskResult, numTasks)
+ pauseRequested := false
+
+ for _, site := range sites {
+ waitGroup.Add(1)
+ go func(wg *sync.WaitGroup, site string, results chan<- TaskResult) {
+ defer wg.Done()
+ inputCopy := make(map[string]interface{})
+ for k, v := range inputs {
+ inputCopy[k] = v
+ }
+ inputCopy["__site"] = site
+
+ for k, v := range inputCopy {
+ var val interface{}
+ val, err = s.traceValue(v, inputCopy, triggerData.Outputs)
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to evaluate input: %v", err)
+ results <- TaskResult{
+ Outputs: nil,
+ Error: err,
+ Site: site,
+ }
+ return
+ }
+ inputCopy[k] = val
+ }
+
+ if _, ok := provider.(*remote.RemoteStageProvider); ok {
+ provider.(*remote.RemoteStageProvider).SetOutputsContext(triggerData.Outputs)
+ }
+
+ if triggerData.Schedule != nil {
+ s.Context.Publish("schedule", v1alpha2.Event{
+ Body: triggerData,
+ })
+ pauseRequested = true
+ results <- TaskResult{
+ Outputs: nil,
+ Error: nil,
+ Site: site,
+ }
+ } else {
+ var outputs map[string]interface{}
+ var pause bool
+ outputs, pause, err = provider.(stage.IStageProvider).Process(ctx, *s.Manager.Context, inputCopy)
+
+ if pause {
+ pauseRequested = true
+ }
+ results <- TaskResult{
+ Outputs: outputs,
+ Error: err,
+ Site: site,
+ }
+ }
+ }(&waitGroup, site, results)
+ }
+
+ waitGroup.Wait()
+ close(results)
+
+ outputs := make(map[string]interface{})
+ delayedExit := false
+ for result := range results {
+ err = result.GetError()
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = fmt.Sprintf("%s: %s", result.Site, err.Error())
+ status.IsActive = false
+ site := result.Site
+ if result.Site == s.Context.SiteInfo.SiteId {
+ site = ""
+ }
+ status.Outputs = carryOutPutsToErrorStatus(nil, err, site)
+ result.Outputs = carryOutPutsToErrorStatus(nil, err, site)
+ log.Errorf(" M (Stage): failed to process stage outputs: %v", err)
+ delayedExit = true
+ }
+ for k, v := range result.Outputs {
+ if result.Site == s.Context.SiteInfo.SiteId {
+ outputs[k] = v
+ } else {
+ outputs[fmt.Sprintf("%s.%s", result.Site, k)] = v
+ }
+ }
+ if result.Site == s.Context.SiteInfo.SiteId {
+ if _, ok := result.Outputs["__status"]; !ok {
+ outputs["__status"] = v1alpha2.OK
+ }
+ } else {
+ key := fmt.Sprintf("%s.__status", result.Site)
+ if _, ok := result.Outputs[key]; !ok {
+ outputs[fmt.Sprintf("%s.__status", result.Site)] = v1alpha2.OK
+ }
+ }
+ }
+ outputs["__campaign"] = triggerData.Campaign
+ outputs["__activation"] = triggerData.Activation
+ outputs["__activationGeneration"] = triggerData.ActivationGeneration
+ outputs["__stage"] = triggerData.Stage
+ outputs["__site"] = s.VendorContext.SiteInfo.SiteId
+ status.Outputs = outputs //TODO: This is newly added 10/3/2023, is this correct?
+ if triggerData.Outputs == nil {
+ triggerData.Outputs = make(map[string]map[string]interface{})
+ }
+ triggerData.Outputs[triggerData.Stage] = outputs
+ if campaign.SelfDriving {
+ if pauseRequested {
+ pendingTask := PendingTask{
+ Sites: sites,
+ OutputContext: triggerData.Outputs,
+ }
+ _, err = s.StateProvider.Upsert(ctx, states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: fmt.Sprintf("%s-%s-%s", triggerData.Campaign, triggerData.Activation, triggerData.ActivationGeneration),
+ Body: pendingTask,
+ },
+ })
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to save pending task: %v", err)
+ return status, activationData
+ }
+ status.Status = v1alpha2.Paused
+ status.IsActive = false
+ return status, activationData
+ }
+
+ parser := utils.NewParser(currentStage.StageSelector)
+ eCtx := s.VendorContext.EvaluationContext.Clone()
+ eCtx.Inputs = triggerData.Inputs
+ if eCtx.Inputs != nil {
+ if v, ok := eCtx.Inputs["context"]; ok {
+ eCtx.Value = v
+ }
+ }
+ eCtx.Outputs = triggerData.Outputs
+ var val interface{}
+ val, err = parser.Eval(*eCtx)
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to evaluate stage selector: %v", err)
+ return status, activationData
+ }
+ sVal := ""
+ if val != nil {
+ sVal = val.(string)
+ }
+ if sVal != "" {
+ if nextStage, ok := campaign.Stages[sVal]; ok {
+ if !delayedExit || nextStage.HandleErrors {
+ status.NextStage = sVal
+ activationData = &v1alpha2.ActivationData{
+ Campaign: triggerData.Campaign,
+ Activation: triggerData.Activation,
+ ActivationGeneration: triggerData.ActivationGeneration,
+ Stage: sVal,
+ Inputs: triggerData.Inputs,
+ Outputs: triggerData.Outputs,
+ Provider: nextStage.Provider,
+ Config: nextStage.Config,
+ TriggeringStage: triggerData.Stage,
+ Schedule: nextStage.Schedule,
+ }
+ } else {
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = fmt.Sprintf("stage %s failed", triggerData.Stage)
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to process stage outputs: %v", status.ErrorMessage)
+ return status, activationData
+ }
+ } else {
+ err = v1alpha2.NewCOAError(nil, status.ErrorMessage, v1alpha2.BadRequest)
+ status.Status = v1alpha2.BadRequest
+ status.ErrorMessage = fmt.Sprintf("stage %s is not found", sVal)
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to find next stage: %v", err)
+ return status, activationData
+ }
+ }
+ status.NextStage = sVal
+ if sVal == "" {
+ status.IsActive = false
+ status.Status = v1alpha2.Done
+ } else {
+ if pauseRequested {
+ status.IsActive = false
+ status.Status = v1alpha2.Paused
+ } else {
+ status.IsActive = true
+ status.Status = v1alpha2.Running
+ }
+ }
+ log.Infof(" M (Stage): stage %s is done", triggerData.Stage)
+ return status, activationData
+ } else {
+ status.Status = v1alpha2.Done
+ status.NextStage = ""
+ status.IsActive = false
+ log.Infof(" M (Stage): stage %s is done (no next stage)", triggerData.Stage)
+ return status, activationData
+ }
+ }
+ err = v1alpha2.NewCOAError(nil, fmt.Sprintf("stage %s is not found", triggerData.Stage), v1alpha2.BadRequest)
+ status.Status = v1alpha2.InternalError
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ log.Errorf(" M (Stage): failed to find stage: %v", err)
+ return status, activationData
+}
+
+func (s *StageManager) traceValue(v interface{}, inputs map[string]interface{}, outputs map[string]map[string]interface{}) (interface{}, error) {
+ switch val := v.(type) {
+ case string:
+ parser := utils.NewParser(val)
+ context := s.Context.VencorContext.EvaluationContext.Clone()
+ context.DeploymentSpec = s.Context.VencorContext.EvaluationContext.DeploymentSpec
+ context.Inputs = inputs
+ context.Outputs = outputs
+ if context.Inputs != nil {
+ if v, ok := context.Inputs["context"]; ok {
+ context.Value = v
+ }
+ }
+ v, err := parser.Eval(*context)
+ if err != nil {
+ return "", err
+ }
+ switch vt := v.(type) {
+ case string:
+ return vt, nil
+ default:
+ return s.traceValue(v, inputs, outputs)
+ }
+ case []interface{}:
+ ret := []interface{}{}
+ for _, v := range val {
+ tv, err := s.traceValue(v, inputs, outputs)
+ if err != nil {
+ return "", err
+ }
+ ret = append(ret, tv)
+ }
+ return ret, nil
+ case map[string]interface{}:
+ ret := map[string]interface{}{}
+ for k, v := range val {
+ tv, err := s.traceValue(v, inputs, outputs)
+ if err != nil {
+ return "", err
+ }
+ ret[k] = tv
+ }
+ return ret, nil
+ default:
+ return val, nil
+ }
+}
+
+func (s *StageManager) HandleActivationEvent(ctx context.Context, actData v1alpha2.ActivationData, campaign model.CampaignSpec, activation model.ActivationState) (*v1alpha2.ActivationData, error) {
+ stage := actData.Stage
+ if _, ok := campaign.Stages[stage]; !ok {
+ stage = campaign.FirstStage
+ }
+ if stage == "" {
+ return nil, v1alpha2.NewCOAError(nil, "no stage found", v1alpha2.BadRequest)
+ }
+ if stageSpec, ok := campaign.Stages[stage]; ok {
+ if activation.Status != nil && activation.Status.Stage != "" && activation.Status.NextStage != stage {
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("stage %s is not the next stage", stage), v1alpha2.BadRequest)
+ }
+ return &v1alpha2.ActivationData{
+ Campaign: actData.Campaign,
+ Activation: actData.Activation,
+ ActivationGeneration: actData.ActivationGeneration,
+ Stage: stage,
+ Inputs: activation.Spec.Inputs,
+ Provider: stageSpec.Provider,
+ Config: stageSpec.Config,
+ TriggeringStage: stage,
+ Schedule: stageSpec.Schedule,
+ }, nil
+ }
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("stage %s is not found", stage), v1alpha2.BadRequest)
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package staging
+
+import (
+ "context"
+ "encoding/json"
+
+ "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/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/queue"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+)
+
+var log = logger.NewLogger("coa.runtime")
+
+type StagingManager struct {
+ managers.Manager
+ QueueProvider queue.IQueueProvider
+ StateProvider states.IStateProvider
+}
+
+const Site_Job_Queue = "site-job-queue"
+
+func (s *StagingManager) 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
+ }
+ queueProvider, err := managers.GetQueueProvider(config, providers)
+ if err == nil {
+ s.QueueProvider = queueProvider
+ } else {
+ return err
+ }
+ stateprovider, err := managers.GetStateProvider(config, providers)
+ if err == nil {
+ s.StateProvider = stateprovider
+ } else {
+ return err
+ }
+ return nil
+}
+func (s *StagingManager) Enabled() bool {
+ return s.Config.Properties["poll.enabled"] == "true"
+}
+func (s *StagingManager) Poll() []error {
+ ctx, span := observability.StartSpan("Staging Manager", context.Background(), &map[string]string{
+ "method": "Poll",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ log.Debug(" M (Staging): Polling...")
+ if s.QueueProvider.Size(Site_Job_Queue) == 0 {
+ return nil
+ }
+ site, err := s.QueueProvider.Dequeue(Site_Job_Queue)
+ if err != nil {
+ log.Errorf(" M (Staging): Failed to poll: %s", err.Error())
+ return []error{err}
+ }
+ siteId := site.(string)
+ catalogs, err := utils.GetCatalogs(
+ ctx,
+ s.VendorContext.SiteInfo.CurrentSite.BaseUrl,
+ s.VendorContext.SiteInfo.CurrentSite.Username,
+ s.VendorContext.SiteInfo.CurrentSite.Password)
+ if err != nil {
+ log.Errorf(" M (Staging): Failed to get catalogs: %s", err.Error())
+ observ_utils.CloseSpanWithError(span, &err)
+ return []error{err}
+ }
+ for _, catalog := range catalogs {
+ cacheId := siteId + "-" + catalog.Spec.Name
+ getRequest := states.GetRequest{
+ ID: cacheId,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "catalogs",
+ },
+ }
+ var entry states.StateEntry
+ entry, err = s.StateProvider.Get(ctx, getRequest)
+ if err == nil && entry.Body != nil && entry.Body.(string) == catalog.Spec.Generation {
+ continue
+ }
+ if err != nil && !v1alpha2.IsNotFound(err) {
+ log.Errorf(" M (Staging): Failed to get catalog %s: %s", catalog.Spec.Name, err.Error())
+ }
+ s.QueueProvider.Enqueue(siteId, v1alpha2.JobData{
+ Id: catalog.Spec.Name,
+ Action: "UPDATE",
+ Body: catalog,
+ })
+ _, err = s.StateProvider.Upsert(ctx, states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: cacheId,
+ Body: catalog.Spec.Generation,
+ },
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "catalogs",
+ },
+ })
+ if err != nil {
+ log.Errorf(" M (Staging): Failed to record catalog %s: %s", catalog.Spec.Name, err.Error())
+ }
+ }
+ return nil
+}
+func (s *StagingManager) Reconcil() []error {
+ return nil
+}
+
+func (s *StagingManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event) error {
+ _, span := observability.StartSpan("Staging Manager", ctx, &map[string]string{
+ "method": "HandleJobEvent",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ var job v1alpha2.JobData
+ jData, _ := json.Marshal(event.Body)
+ err = json.Unmarshal(jData, &job)
+ if err != nil {
+ err = v1alpha2.NewCOAError(nil, "event body is not a job", v1alpha2.BadRequest)
+ return err
+ }
+ s.QueueProvider.Enqueue(Site_Job_Queue, event.Metadata["site"])
+ return s.QueueProvider.Enqueue(event.Metadata["site"], job)
+}
+func (s *StagingManager) GetABatchForSite(site string, count int) ([]v1alpha2.JobData, error) {
+ //TODO: this should return a group of jobs as optimization
+ s.QueueProvider.Enqueue(Site_Job_Queue, site)
+ if s.QueueProvider.Size(site) == 0 {
+ return nil, nil
+ }
+ items := []v1alpha2.JobData{}
+ itemCount := 0
+ for {
+ stackElement, err := s.QueueProvider.Dequeue(site)
+ if err != nil {
+ return nil, err
+ }
+ if job, ok := stackElement.(v1alpha2.JobData); ok {
+ items = append(items, job)
+ itemCount++
+ } else {
+ s.QueueProvider.Enqueue(site, stackElement)
+ }
+ if itemCount == count || s.QueueProvider.Size(site) == 0 {
+ break
+ }
+ }
+ return items, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package target
+
+import (
+ "context"
+ "encoding/json"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "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/probe"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/reference"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/reporter"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/uploader"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var log = logger.NewLogger("coa.runtime")
+var lock sync.Mutex
+
+type TargetManager struct {
+ managers.Manager
+ ReferenceProvider reference.IReferenceProvider
+ ProbeProvider probe.IProbeProvider
+ UploaderProvider uploader.IUploader
+ Reporter reporter.IReporter
+}
+
+type Device struct {
+ Object Object
+}
+type Object struct {
+ ApiVersion string `json:"apiVersion`
+ Kind string `json:"kind"`
+ Metadata map[string]interface{} `json:"metadata"`
+ Spec DeviceSpec `json:"spec"`
+}
+type DeviceSpec struct {
+ Properties map[string]string `json:"properties"`
+}
+
+func (s *TargetManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error {
+
+ probeProvider, err := managers.GetProbeProvider(config, providers)
+ if err == nil {
+ s.ProbeProvider = probeProvider
+ } else {
+ return err
+ }
+
+ referenceProvider, err := managers.GetReferenceProvider(config, providers)
+ if err == nil {
+ s.ReferenceProvider = referenceProvider
+ } else {
+ return err
+ }
+
+ uploaderProvider, err := managers.GetUploaderProvider(config, providers)
+ if err == nil {
+ s.UploaderProvider = uploaderProvider
+ } else {
+ return err
+ }
+
+ reporterProvider, err := managers.GetReporter(config, providers)
+ if err == nil {
+ s.Reporter = reporterProvider
+ } else {
+ return err
+ }
+
+ return nil
+}
+
+func (s *TargetManager) Apply(ctx context.Context, target model.TargetSpec) error {
+ return nil
+}
+func (s *TargetManager) Get(ctx context.Context) (model.TargetSpec, error) {
+ return model.TargetSpec{}, nil
+}
+func (s *TargetManager) Remove(ctx context.Context, target model.TargetSpec) error {
+ return nil
+}
+func (s *TargetManager) Enabled() bool {
+ return s.Config.Properties["poll.enabled"] == "true"
+}
+func (s *TargetManager) Poll() []error {
+ target := s.ReferenceProvider.TargetID()
+
+ ret, err := s.ReferenceProvider.List(target+"=true", "", "default", model.FabricGroup, "devices", "v1", "v1alpha2.ReferenceK8sCRD")
+ if err != nil {
+ return []error{err}
+ }
+ jsonData, _ := json.Marshal(ret)
+ devices := make([]Device, 0)
+ json.Unmarshal(jsonData, &devices)
+ log.Debugf("polling %d devices...", len(devices))
+ errors := make([]error, 0)
+
+ first := true
+ for _, device := range devices {
+ user := ""
+ if u, ok := device.Object.Spec.Properties["user"]; ok {
+ user = u
+ }
+ password := ""
+ if p, ok := device.Object.Spec.Properties["password"]; ok {
+ password = p
+ }
+ ip := ""
+ if i, ok := device.Object.Spec.Properties["ip"]; ok {
+ ip = i
+ }
+ name := device.Object.Metadata["name"].(string)
+ if ip != "" {
+ if user != "" && password != "" {
+ log.Debugf("taking snapshot from rtsp://%s:%s@%s...", user, "<password>", strings.ReplaceAll(ip, "rtsp://", ""))
+ } else {
+ log.Debugf("taking snapshot from rtsp://%s...", strings.ReplaceAll(ip, "rtsp://", ""))
+ }
+ ret, err := s.ProbeProvider.Probe(user, password, ip, name)
+ if err != nil {
+ log.Debugf("failed to probe device: %s", err.Error())
+ errors = append(errors, err)
+ errors = append(errors, s.reportStatus(name, target, "", "disconnected", "disconnected", first, err.Error())...)
+ continue
+ }
+ if v, ok := ret["snapshot"]; ok {
+ file, err := os.Open(v)
+ if err != nil {
+ log.Debugf("failed to open local file: %s", err.Error())
+ errors = append(errors, err)
+ errors = append(errors, s.reportStatus(name, target, "", "connected", "connected", first, err.Error())...)
+ continue
+ }
+ data, err := ioutil.ReadAll(file)
+ if err != nil {
+ log.Debugf("failed to read local file: %s", err.Error())
+ errors = append(errors, err)
+ errors = append(errors, s.reportStatus(name, target, "", "connected", "connected", first, err.Error())...)
+ continue
+ }
+ fileName := filepath.Base(v)
+ str, err := s.UploaderProvider.Upload(fileName, data)
+ if err != nil {
+ log.Debugf("failed to upload snapshot: %s", err.Error())
+ errors = append(errors, err)
+ errors = append(errors, s.reportStatus(name, target, "", "connected", "connected", first, err.Error())...)
+ continue
+ }
+ log.Debugf("file is uploaded to %s", str)
+ errors = append(errors, s.reportStatus(name, target, str, "connected", "connected", first, "")...)
+ }
+ } else {
+ errors = append(errors, s.reportStatus(name, target, "", "disconnected", "disconnected", first, "device ip is not set")...)
+ }
+ first = false
+ }
+ return errors
+}
+func (s *TargetManager) reportStatus(deviceName string, targetName string, snapshot string, targetStatus string, deviceStatus string, overwrite bool, errStr string) []error {
+ ret := make([]error, 0)
+ report := make(map[string]string)
+ report[targetName+".status"] = targetStatus
+ if snapshot != "" {
+ report["snapshot"] = snapshot
+ }
+ if errStr != "" {
+ report[targetName+".err"] = errStr
+ }
+ err := s.Reporter.Report(deviceName, "default", model.FabricGroup, "devices", "v1", report, false) //can't overwrite device state properties as other targets may be reporting as well
+ if err != nil {
+ log.Debugf("failed to report device status: %s", err.Error())
+ ret = append(ret, err)
+ }
+ report = make(map[string]string)
+ report[deviceName+".status"] = deviceStatus
+ if errStr != "" {
+ report[deviceName+".err"] = errStr
+ }
+ err = s.Reporter.Report(targetName, "default", model.FabricGroup, "targets", "v1", report, overwrite)
+ if err != nil {
+ log.Debugf("failed to report target status: %s", err.Error())
+ ret = append(ret, err)
+ }
+ return ret
+}
+func (s *TargetManager) Reconcil() []error {
+ log.Debug("Rconciling....")
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package targets
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "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/observability"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/registry"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+)
+
+type TargetsManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+ RegistryProvider registry.IRegistryProvider
+}
+
+func (s *TargetsManager) 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 *TargetsManager) DeleteSpec(ctx context.Context, name string, scope string) error {
+ ctx, span := observability.StartSpan("Targets Manager", ctx, &map[string]string{
+ "method": "DeleteSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = t.StateProvider.Delete(ctx, states.DeleteRequest{
+ ID: name,
+ Metadata: map[string]string{
+ "scope": scope,
+ "group": model.FabricGroup,
+ "version": "v1",
+ "resource": "targets",
+ },
+ })
+ return err
+}
+
+func (t *TargetsManager) UpsertSpec(ctx context.Context, name string, scope string, spec model.TargetSpec) error {
+ ctx, span := observability.StartSpan("Targets Manager", ctx, &map[string]string{
+ "method": "UpsertSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ upsertRequest := states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: name,
+ Body: map[string]interface{}{
+ "apiVersion": model.FabricGroup + "/v1",
+ "kind": "Target",
+ "metadata": map[string]interface{}{
+ "name": name,
+ },
+ "spec": spec,
+ },
+ ETag: spec.Generation,
+ },
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Target", "metadata": {"name": "${{$target()}}"}}`, model.FabricGroup),
+ "scope": scope,
+ "group": model.FabricGroup,
+ "version": "v1",
+ "resource": "targets",
+ },
+ }
+ _, err = t.StateProvider.Upsert(ctx, upsertRequest)
+ return err
+}
+
+// Caller need to explicitly set scope in current.Metadata!
+func (t *TargetsManager) ReportState(ctx context.Context, current model.TargetState) (model.TargetState, error) {
+ ctx, span := observability.StartSpan("Targets Manager", ctx, &map[string]string{
+ "method": "ReportState",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ getRequest := states.GetRequest{
+ ID: current.Id,
+ Metadata: current.Metadata,
+ }
+ target, err := t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ observ_utils.CloseSpanWithError(span, &err)
+ return model.TargetState{}, err
+ }
+
+ dict := target.Body.(map[string]interface{})
+
+ specCol := dict["spec"].(model.TargetSpec)
+
+ delete(dict, "spec")
+ if dict["status"] == nil {
+ dict["status"] = make(map[string]interface{})
+ }
+ status := dict["status"]
+
+ j, _ := json.Marshal(status)
+ var rStatus map[string]interface{}
+ err = json.Unmarshal(j, &rStatus)
+ if err != nil {
+ return model.TargetState{}, err
+ }
+ j, _ = json.Marshal(rStatus["properties"])
+ var rProperties map[string]string
+ err = json.Unmarshal(j, &rProperties)
+ if err != nil {
+ return model.TargetState{}, err
+ }
+ if rProperties == nil {
+ rProperties = make(map[string]string)
+ }
+ for k, v := range current.Status {
+ rProperties[k] = v
+ }
+
+ dict["status"].(map[string]interface{})["properties"] = rProperties
+
+ target.Body = dict
+
+ updateRequest := states.UpsertRequest{
+ Value: target,
+ Metadata: current.Metadata,
+ }
+
+ _, err = t.StateProvider.Upsert(ctx, updateRequest)
+ if err != nil {
+ return model.TargetState{}, err
+ }
+
+ return model.TargetState{
+ Id: current.Id,
+ Metadata: specCol.Metadata,
+ Status: rProperties,
+ }, nil
+}
+func (t *TargetsManager) ListSpec(ctx context.Context, scope string) ([]model.TargetState, error) {
+ ctx, span := observability.StartSpan("Targets Manager", ctx, &map[string]string{
+ "method": "ListSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ listRequest := states.ListRequest{
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FabricGroup,
+ "resource": "targets",
+ "scope": scope,
+ },
+ }
+ targets, _, err := t.StateProvider.List(ctx, listRequest)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]model.TargetState, 0)
+ for _, t := range targets {
+ var rt model.TargetState
+ rt, err = getTargetState(t.ID, t.Body, t.ETag)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, rt)
+ }
+ return ret, nil
+}
+
+func getTargetState(id string, body interface{}, etag string) (model.TargetState, error) {
+ dict := body.(map[string]interface{})
+ spec := dict["spec"]
+ status := dict["status"]
+
+ j, _ := json.Marshal(spec)
+ var rSpec model.TargetSpec
+ err := json.Unmarshal(j, &rSpec)
+ if err != nil {
+ return model.TargetState{}, err
+ }
+
+ j, _ = json.Marshal(status)
+ var rStatus map[string]interface{}
+ err = json.Unmarshal(j, &rStatus)
+ if err != nil {
+ return model.TargetState{}, err
+ }
+ j, _ = json.Marshal(rStatus["properties"])
+ var rProperties map[string]string
+ err = json.Unmarshal(j, &rProperties)
+ if err != nil {
+ return model.TargetState{}, err
+ }
+ rSpec.Generation = etag
+
+ scope, exist := dict["scope"]
+ var s string
+ if !exist {
+ s = "default"
+ } else {
+ s = scope.(string)
+ }
+
+ state := model.TargetState{
+ Id: id,
+ Scope: s,
+ Spec: &rSpec,
+ Status: rProperties,
+ }
+ return state, nil
+}
+
+func (t *TargetsManager) GetSpec(ctx context.Context, id string, scope string) (model.TargetState, error) {
+ ctx, span := observability.StartSpan("Targets Manager", ctx, &map[string]string{
+ "method": "GetSpec",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ getRequest := states.GetRequest{
+ ID: id,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FabricGroup,
+ "resource": "targets",
+ "scope": scope,
+ },
+ }
+ target, err := t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ return model.TargetState{}, err
+ }
+
+ ret, err := getTargetState(id, target.Body, target.ETag)
+ if err != nil {
+ return model.TargetState{}, err
+ }
+ return ret, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package users
+
+import (
+ "context"
+ "fmt"
+ "hash/fnv"
+
+ "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/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var log = logger.NewLogger("coa.runtime")
+
+type UsersManager struct {
+ managers.Manager
+ StateProvider states.IStateProvider
+}
+
+type UserState struct {
+ Id string `json:"id"`
+ PasswordHash string `json:"passwordHash,omitempty"`
+ Roles []string `json:"roles,omitempty"`
+}
+
+func (s *UsersManager) Init(context *contexts.VendorContext, config managers.ManagerConfig, providers map[string]providers.IProvider) error {
+ stateprovider, err := managers.GetStateProvider(config, providers)
+ if err == nil {
+ s.StateProvider = stateprovider
+ } else {
+ return err
+ }
+
+ return nil
+}
+func (t *UsersManager) DeleteUser(ctx context.Context, name string) error {
+ ctx, span := observability.StartSpan("Users Manager", ctx, &map[string]string{
+ "method": "DeleteUser",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = t.StateProvider.Delete(ctx, states.DeleteRequest{
+ ID: name,
+ })
+ return err
+}
+
+func hash(name string, s string) string {
+ h := fnv.New32a()
+ h.Write([]byte(name + "." + s + ".salt"))
+ return fmt.Sprintf("H%d", h.Sum32())
+}
+
+func (t *UsersManager) UpsertUser(ctx context.Context, name string, password string, roles []string) error {
+ ctx, span := observability.StartSpan("Users Manager", ctx, &map[string]string{
+ "method": "UpsertUser",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ log.Debug(" M (Users) : upsert user")
+ upsertRequest := states.UpsertRequest{
+ Value: states.StateEntry{
+ ID: name,
+ Body: UserState{
+ Id: name,
+ PasswordHash: hash(name, password),
+ Roles: roles,
+ },
+ },
+ }
+ _, err = t.StateProvider.Upsert(ctx, upsertRequest)
+ if err != nil {
+ log.Debugf(" M (Users) : failed to upsert user - %s", err)
+ return err
+ }
+ return nil
+}
+func (t *UsersManager) CheckUser(ctx context.Context, name string, password string) ([]string, bool) {
+ ctx, span := observability.StartSpan("Users Manager", ctx, &map[string]string{
+ "method": "CheckUser",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ log.Debug(" M (Users) : check user")
+ getRequest := states.GetRequest{
+ ID: name,
+ }
+ user, err := t.StateProvider.Get(ctx, getRequest)
+ if err != nil {
+ log.Debugf(" M (Users) : failed to read user - %s", err)
+ return nil, false
+ }
+
+ if v, ok := user.Body.(UserState); ok {
+ if hash(name, password) == v.PasswordHash {
+ log.Debug(" M (Users) : user authenticated")
+ return v.Roles, true
+ }
+ }
+ log.Debug(" M (Users) : authentication failed")
+ return nil, false
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import "errors"
+
+// +kubebuilder:object:generate=true
+type BindingSpec struct {
+ Role string `json:"role"`
+ Provider string `json:"provider"`
+ Config map[string]string `json:"config,omitempty"`
+}
+
+func (c BindingSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(BindingSpec)
+ if !ok {
+ return false, errors.New("parameter is not a BindingSpec type")
+ }
+
+ if c.Role != otherC.Role {
+ return false, nil
+ }
+
+ if c.Provider != otherC.Provider {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Config, otherC.Config, nil) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+ "reflect"
+
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+)
+
+type CampaignState struct {
+ Id string `json:"id"`
+ Spec *CampaignSpec `json:"spec,omitempty"`
+}
+
+type ActivationState struct {
+ Id string `json:"id"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Spec *ActivationSpec `json:"spec,omitempty"`
+ Status *ActivationStatus `json:"status,omitempty"`
+}
+type StageSpec struct {
+ Name string `json:"name,omitempty"`
+ Contexts string `json:"contexts,omitempty"`
+ Provider string `json:"provider,omitempty"`
+ Config interface{} `json:"config,omitempty"`
+ StageSelector string `json:"stageSelector,omitempty"`
+ Inputs map[string]interface{} `json:"inputs,omitempty"`
+ HandleErrors bool `json:"handleErrors,omitempty"`
+ Schedule *v1alpha2.ScheduleSpec `json:"schedule,omitempty"`
+}
+
+func (s StageSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherS, ok := other.(StageSpec)
+ if !ok {
+ return false, errors.New("parameter is not a StageSpec type")
+ }
+
+ if s.Name != otherS.Name {
+ return false, nil
+ }
+
+ if s.Provider != otherS.Provider {
+ return false, nil
+ }
+
+ if !reflect.DeepEqual(s.Config, otherS.Config) {
+ return false, nil
+ }
+
+ if s.StageSelector != otherS.StageSelector {
+ return false, nil
+ }
+
+ if !reflect.DeepEqual(s.Inputs, otherS.Inputs) {
+ return false, nil
+ }
+
+ if !reflect.DeepEqual(s.Schedule, otherS.Schedule) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+type ActivationStatus struct {
+ Stage string `json:"stage"`
+ NextStage string `json:"nextStage,omitempty"`
+ Inputs map[string]interface{} `json:"inputs,omitempty"`
+ Outputs map[string]interface{} `json:"outputs,omitempty"`
+ Status v1alpha2.State `json:"status,omitempty"`
+ ErrorMessage string `json:"errorMessage,omitempty"`
+ IsActive bool `json:"isActive,omitempty"`
+ ActivationGeneration string `json:"activationGeneration,omitempty"`
+ UpdateTime string `json:"updateTime,omitempty"`
+}
+
+type ActivationSpec struct {
+ Campaign string `json:"campaign,omitempty"`
+ Name string `json:"name,omitempty"`
+ Stage string `json:"stage,omitempty"`
+ Inputs map[string]interface{} `json:"inputs,omitempty"`
+ Generation string `json:"generation,omitempty"`
+}
+
+func (c ActivationSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(ActivationSpec)
+ if !ok {
+ return false, errors.New("parameter is not a ActivationSpec type")
+ }
+
+ if c.Campaign != otherC.Campaign {
+ return false, nil
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ if c.Stage != otherC.Stage {
+ return false, nil
+ }
+
+ if !reflect.DeepEqual(c.Inputs, otherC.Inputs) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+type CampaignSpec struct {
+ Name string `json:"name,omitempty"`
+ FirstStage string `json:"firstStage,omitempty"`
+ Stages map[string]StageSpec `json:"stages,omitempty"`
+ SelfDriving bool `json:"selfDriving,omitempty"`
+}
+
+func (c CampaignSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(CampaignSpec)
+ if !ok {
+ return false, errors.New("parameter is not a CampaignSpec type")
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ if c.FirstStage != otherC.FirstStage {
+ return false, nil
+ }
+
+ if c.SelfDriving != otherC.SelfDriving {
+ return false, nil
+ }
+
+ if len(c.Stages) != len(otherC.Stages) {
+ return false, nil
+ }
+
+ if len(c.Stages) != len(otherC.Stages) {
+ return false, nil
+ }
+ for i, stage := range c.Stages {
+ otherStage := otherC.Stages[i]
+
+ if eq, err := stage.DeepEquals(otherStage); err != nil || !eq {
+ return eq, err
+ }
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+ "reflect"
+)
+
+// TODO: all state objects should converge to this paradigm: id, spec and status
+type CatalogState struct {
+ Id string `json:"id"`
+ Spec *CatalogSpec `json:"spec,omitempty"`
+ Status *CatalogStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:generate=true
+type ObjectRef struct {
+ SiteId string `json:"siteId"`
+ Name string `json:"name"`
+ Group string `json:"group"`
+ Version string `json:"version"`
+ Kind string `json:"kind"`
+ Scope string `json:"scope"`
+ Address string `json:"address,omitempty"`
+ Generation string `json:"generation,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+}
+type CatalogSpec struct {
+ SiteId string `json:"siteId"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Properties map[string]interface{} `json:"properties"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ ParentName string `json:"parentName,omitempty"`
+ ObjectRef ObjectRef `json:"objectRef,omitempty"`
+ Generation string `json:"generation,omitempty"`
+}
+
+type CatalogStatus struct {
+ Properties map[string]string `json:"properties"`
+}
+
+func (c CatalogSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(CatalogSpec)
+ if !ok {
+ return false, errors.New("parameter is not a CatalogSpec type")
+ }
+
+ if c.SiteId != otherC.SiteId {
+ return false, nil
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ if c.ParentName != otherC.ParentName {
+ return false, nil
+ }
+
+ if c.Generation != otherC.Generation {
+ return false, nil
+ }
+
+ if !reflect.DeepEqual(c.Properties, otherC.Properties) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+// INode interface
+func (s CatalogState) GetId() string {
+ return s.Id
+}
+func (s CatalogState) GetParent() string {
+ if s.Spec != nil {
+ return s.Spec.ParentName
+ }
+ return ""
+}
+func (s CatalogState) GetType() string {
+ if s.Spec != nil {
+ return s.Spec.Type
+ }
+ return ""
+}
+func (s CatalogState) GetProperties() map[string]interface{} {
+ if s.Spec != nil {
+ return s.Spec.Properties
+ }
+ return nil
+}
+
+// IEdge interface
+func (s CatalogState) GetFrom() string {
+ if s.Spec != nil {
+ if s.Spec.Type == "edge" {
+ if s.Spec.Metadata != nil {
+ if from, ok := s.Spec.Metadata["from"]; ok {
+ return from
+ }
+ }
+ }
+ }
+ return ""
+}
+
+func (s CatalogState) GetTo() string {
+ if s.Spec != nil {
+ if s.Spec.Type == "edge" {
+ if s.Spec.Metadata != nil {
+ if to, ok := s.Spec.Metadata["to"]; ok {
+ return to
+ }
+ }
+ }
+ }
+ return ""
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+ "reflect"
+)
+
+type ComponentSpec struct {
+ Name string `json:"name"`
+ Type string `json:"type,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Properties map[string]interface{} `json:"properties,omitempty"`
+ Parameters map[string]string `json:"parameters,omitempty"`
+ Routes []RouteSpec `json:"routes,omitempty"`
+ Constraints string `json:"constraints,omitempty"`
+ Dependencies []string `json:"dependencies,omitempty"`
+ Skills []string `json:"skills,omitempty"`
+}
+
+func (c ComponentSpec) DeepEquals(other IDeepEquals) (bool, error) { // avoid using reflect, which has performance problems
+ otherC, ok := other.(ComponentSpec)
+ if !ok {
+ return false, errors.New("parameter is not a ComponentSpec type")
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ if !reflect.DeepEqual(c.Properties, otherC.Properties) {
+ return false, nil
+ }
+ if !StringMapsEqual(c.Metadata, otherC.Metadata, []string{"SYMPHONY_AGENT_ADDRESS"}) {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Routes, otherC.Routes) {
+ return false, nil
+ }
+
+ // if c.Constraints != otherC.Constraints { Can't compare constraints as components from actual envrionments don't have constraints
+ // return false, nil
+ // }
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+
+ go_slices "golang.org/x/exp/slices"
+)
+
+type DeploymentSpec struct {
+ SolutionName string `json:"solutionName"`
+ Solution SolutionSpec `json:"solution"`
+ Instance InstanceSpec `json:"instance"`
+ Targets map[string]TargetSpec `json:"targets"`
+ Devices []DeviceSpec `json:"devices,omitempty"`
+ Assignments map[string]string `json:"assignments,omitempty"`
+ ComponentStartIndex int `json:"componentStartIndex,omitempty"`
+ ComponentEndIndex int `json:"componentEndIndex,omitempty"`
+ ActiveTarget string `json:"activeTarget,omitempty"`
+ Generation string `json:"generation,omitempty"`
+}
+
+func (d DeploymentSpec) GetComponentSlice() []ComponentSpec {
+ components := d.Solution.Components
+ if d.ComponentStartIndex >= 0 && d.ComponentEndIndex >= 0 && d.ComponentEndIndex > d.ComponentStartIndex {
+ components = components[d.ComponentStartIndex:d.ComponentEndIndex]
+ }
+ return components
+}
+
+func (c DeploymentSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(DeploymentSpec)
+ if !ok {
+ return false, errors.New("parameter is not a DeploymentSpec type")
+ }
+
+ if c.SolutionName != otherC.SolutionName {
+ return false, nil
+ }
+
+ equal, err := c.Solution.DeepEquals(otherC.Solution)
+ if err != nil {
+ return false, err
+ }
+
+ if !equal {
+ return false, nil
+ }
+
+ equal, err = c.Instance.DeepEquals(otherC.Instance)
+ if err != nil {
+ return false, err
+ }
+
+ if !equal {
+ return false, nil
+ }
+
+ if !mapsEqual(c.Targets, otherC.Targets, nil) {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Devices, otherC.Devices) {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Assignments, otherC.Assignments, nil) {
+ return false, nil
+ }
+
+ if c.ComponentStartIndex != otherC.ComponentStartIndex {
+ return false, nil
+ }
+
+ if c.ComponentEndIndex != otherC.ComponentEndIndex {
+ return false, nil
+ }
+
+ if c.ActiveTarget != otherC.ActiveTarget {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+func mapsEqual(a map[string]TargetSpec, b map[string]TargetSpec, ignoredMissingKeys []string) bool {
+ for k, v := range a {
+ if bv, ok := b[k]; ok {
+ equal, err := bv.DeepEquals(v)
+ if err != nil || !equal {
+ return false
+ }
+
+ } else {
+ if !go_slices.Contains(ignoredMissingKeys, k) {
+ return false
+ }
+
+ }
+
+ }
+
+ for k, v := range b {
+ if bv, ok := a[k]; ok {
+ equal, err := bv.DeepEquals(v)
+ if err != nil || !equal {
+ return false
+ }
+
+ } else {
+ if !go_slices.Contains(ignoredMissingKeys, k) {
+ return false
+ }
+
+ }
+
+ }
+
+ return true
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import "errors"
+
+type (
+ // DeviceState defines the current state of the device
+ DeviceState struct {
+ Id string `json:"id"`
+ Spec *DeviceSpec `json:"spec,omitempty"`
+ }
+ // DeviceSpec defines the spec properties of the DeviceState
+ // +kubebuilder:object:generate=true
+ DeviceSpec struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Properties map[string]string `json:"properties,omitempty"`
+ Bindings []BindingSpec `json:"bindings,omitempty"`
+ }
+)
+
+func (c DeviceSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(DeviceSpec)
+ if !ok {
+ return false, errors.New("parameter is not a DeviceSpec type")
+ }
+
+ if c.DisplayName != otherC.DisplayName {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Properties, otherC.Properties, nil) {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Bindings, otherC.Bindings) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+type (
+ ConnectionSpec struct {
+ Node string `json:"node"`
+ Route string `json:"route"`
+ }
+ EdgeSpec struct {
+ Source ConnectionSpec `json:"source"`
+ Target ConnectionSpec `json:"target"`
+ }
+)
+
+func (c ConnectionSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherSpec, ok := other.(*ConnectionSpec)
+ if !ok {
+ return false, nil
+ }
+ if c.Node != otherSpec.Node {
+ return false, nil
+ }
+ if c.Route != otherSpec.Route {
+ return false, nil
+ }
+ return true, nil
+}
+func (c EdgeSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherSpec, ok := other.(*EdgeSpec)
+ if !ok {
+ return false, nil
+ }
+ equal, err := c.Source.DeepEquals(&otherSpec.Source)
+ if err != nil {
+ return false, err
+ }
+ if !equal {
+ return false, nil
+ }
+ equal, err = c.Target.DeepEquals(&otherSpec.Target)
+ if err != nil {
+ return false, err
+ }
+ if !equal {
+ return false, nil
+ }
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import "errors"
+
+// +kubebuilder:object:generate=true
+type FilterSpec struct {
+ Direction string `json:"direction"`
+ Type string `json:"type"`
+ Parameters map[string]string `json:"parameters,omitempty"`
+}
+
+func (c FilterSpec) DeepEquals(other IDeepEquals) (bool, error) { // avoid using reflect, which has performance problems
+ otherC, ok := other.(FilterSpec)
+ if !ok {
+ return false, errors.New("parameter is not a FilterSpec type")
+ }
+
+ if c.Direction != otherC.Direction {
+ return false, nil
+ }
+
+ if c.Type != otherC.Type {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Parameters, otherC.Parameters, nil) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import "errors"
+
+type (
+
+ // InstanceState defines the current state of the instance
+ InstanceState struct {
+ Id string `json:"id"`
+ Scope string `json:"scope"`
+ Spec *InstanceSpec `json:"spec,omitempty"`
+ Status map[string]string `json:"status,omitempty"`
+ }
+
+ // InstanceSpec defines the spec property of the InstanceState
+ // +kubebuilder:object:generate=true
+ InstanceSpec struct {
+ Name string `json:"name"`
+ DisplayName string `json:"displayName,omitempty"`
+ Scope string `json:"scope,omitempty"`
+ Parameters map[string]string `json:"parameters,omitempty"` //TODO: Do we still need this?
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Solution string `json:"solution"`
+ Target TargetSelector `json:"target,omitempty"`
+ Topologies []TopologySpec `json:"topologies,omitempty"`
+ Pipelines []PipelineSpec `json:"pipelines,omitempty"`
+ Arguments map[string]map[string]string `json:"arguments,omitempty"`
+ Generation string `json:"generation,omitempty"`
+ // Defines the version of a particular resource
+ Version string `json:"version,omitempty"`
+ }
+
+ // TargertRefSpec defines the target the instance will deploy to
+ // +kubebuilder:object:generate=true
+ TargetSelector struct {
+ Name string `json:"name,omitempty"`
+ Selector map[string]string `json:"selector,omitempty"`
+ }
+
+ // PipelineSpec defines the desired pipeline of the instance
+ // +kubebuilder:object:generate=true
+ PipelineSpec struct {
+ Name string `json:"name"`
+ Skill string `json:"skill"`
+ Parameters map[string]string `json:"parameters,omitempty"`
+ }
+)
+
+func (c TargetSelector) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(TargetSelector)
+ if !ok {
+ return false, errors.New("parameter is not a TargetSelector type")
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Selector, otherC.Selector, nil) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+func (c TopologySpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(TopologySpec)
+ if !ok {
+ return false, errors.New("parameter is not a TopologySpec type")
+ }
+
+ if c.Device != otherC.Device {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Selector, otherC.Selector, nil) {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Bindings, otherC.Bindings) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+func (c PipelineSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(PipelineSpec)
+ if !ok {
+ return false, errors.New("parameter is not a PipelineSpec type")
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ if c.Skill != otherC.Skill {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Parameters, otherC.Parameters, nil) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+func (c InstanceSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(InstanceSpec)
+ if !ok {
+ return false, errors.New("parameter is not a InstanceSpec type")
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ if c.DisplayName != otherC.DisplayName {
+ return false, nil
+ }
+
+ if c.Scope != otherC.Scope {
+ return false, nil
+ }
+
+ // TODO: These are not compared in current version. Metadata is usually not considred part of the state so
+ // it's reasonable not to compare. The parameters (same arguments apply to arguments below) are dynamic so
+ // comparision is unpredictable. Should we not compare the arguments as well? Or, should we get rid of the
+ // dynamic things altoghter so everyting is explicitly declared? I feel we are mixing some templating features
+ // into the object model.
+
+ // if !StringMapsEqual(c.Parameters, otherC.Parameters, nil) {
+ // return false, nil
+ // }
+
+ // if !StringMapsEqual(c.Metadata, otherC.Metadata, nil) {
+ // return false, nil
+ // }
+
+ equal, err := c.Target.DeepEquals(otherC.Target)
+ if err != nil {
+ return false, err
+ }
+
+ if !equal {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Topologies, otherC.Topologies) {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Pipelines, otherC.Pipelines) {
+ return false, nil
+ }
+
+ if !StringStringMapsEqual(c.Arguments, otherC.Arguments, nil) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+type ModelState struct {
+ Id string `json:"id"`
+ Spec *ModelSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:generate=true
+type ModelSpec struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Properties map[string]string `json:"properties,omitempty"`
+ Constraints string `json:"constraints,omitempty"`
+ Bindings []BindingSpec `json:"bindings,omitempty"`
+}
+
+const (
+ AppPackage = "app.package"
+ AppImage = "app.image"
+ ContainerImage = "container.image"
+)
+
+func (c ModelSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherModelSpec, ok := other.(*ModelSpec)
+ if !ok {
+ return false, nil
+ }
+ if c.DisplayName != otherModelSpec.DisplayName {
+ return false, nil
+ }
+ if c.Constraints != otherModelSpec.Constraints {
+ return false, nil
+ }
+ if !StringMapsEqual(c.Properties, otherModelSpec.Properties, nil) {
+ return false, nil
+ }
+ if !SlicesEqual(c.Bindings, otherModelSpec.Bindings) {
+ return false, nil
+ }
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+// +kubebuilder:object:generate=true
+type NodeSpec struct {
+ Id string `json:"id"`
+ NodeType string `json:"type"`
+ Name string `json:"name"`
+ Configurations map[string]string `json:"configurations,omitempty"`
+ Inputs []RouteSpec `json:"inputs,omitempty"`
+ Outputs []RouteSpec `json:"outputs,omitempty"`
+ Model string `json:"model,omitempty"`
+}
+
+func (n NodeSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherSpec, ok := other.(NodeSpec)
+ if !ok {
+ return false, nil
+ }
+ if n.Id != otherSpec.Id {
+ return false, nil
+ }
+ if n.NodeType != otherSpec.NodeType {
+ return false, nil
+ }
+ if n.Name != otherSpec.Name {
+ return false, nil
+ }
+ if n.Model != otherSpec.Model {
+ return false, nil
+ }
+ if !StringMapsEqual(n.Configurations, otherSpec.Configurations, nil) {
+ return false, nil
+ }
+ if !SlicesEqual(n.Inputs, otherSpec.Inputs) {
+ return false, nil
+ }
+ if !SlicesEqual(n.Outputs, otherSpec.Outputs) {
+ return false, nil
+ }
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "strings"
+
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+)
+
+type DeploymentPlan struct {
+ Steps []DeploymentStep
+}
+type DeploymentStep struct {
+ Target string
+ Components []ComponentStep
+ Role string
+ IsFirst bool
+}
+type ComponentStep struct {
+ Action string `json:"action"`
+ Component ComponentSpec `json:"component"`
+}
+
+type TargetDesc struct {
+ Name string
+ Spec TargetSpec
+}
+type ByTargetName []TargetDesc
+
+func (p ByTargetName) Len() int { return len(p) }
+func (p ByTargetName) Less(i, j int) bool { return p[i].Name < p[j].Name }
+func (p ByTargetName) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
+
+type DeploymentState struct {
+ Components []ComponentSpec
+ Targets []TargetDesc
+ TargetComponent map[string]string
+}
+
+func (s DeploymentStep) PrepareResultMap() map[string]ComponentResultSpec {
+ ret := make(map[string]ComponentResultSpec)
+ for _, c := range s.Components {
+ ret[c.Component.Name] = ComponentResultSpec{
+ Status: v1alpha2.Untouched,
+ Message: "",
+ }
+ }
+ return ret
+}
+func (s DeploymentStep) GetComponents() []ComponentSpec {
+ ret := make([]ComponentSpec, 0)
+ for _, c := range s.Components {
+ ret = append(ret, c.Component)
+ }
+ return ret
+}
+func (s DeploymentStep) GetUpdatedComponents() []ComponentSpec {
+ ret := make([]ComponentSpec, 0)
+ for _, c := range s.Components {
+ if c.Action == "update" {
+ ret = append(ret, c.Component)
+ }
+ }
+ return ret
+}
+func (s DeploymentStep) GetDeletedComponents() []ComponentSpec {
+ ret := make([]ComponentSpec, 0)
+ for _, c := range s.Components {
+ if c.Action == "delete" {
+ ret = append(ret, c.Component)
+ }
+ }
+ return ret
+}
+func (s DeploymentStep) GetUpdatedComponentSteps() []ComponentStep {
+ ret := make([]ComponentStep, 0)
+ for _, c := range s.Components {
+ if c.Action == "update" {
+ ret = append(ret, c)
+ }
+ }
+ return ret
+}
+func (t *DeploymentState) MarkRemoveAll() {
+ for k, v := range t.TargetComponent {
+ if !strings.HasPrefix(v, "-") {
+ t.TargetComponent[k] = "-" + v
+ }
+ }
+}
+func (t *DeploymentState) ClearAllRemoved() {
+ for k, v := range t.TargetComponent {
+ if strings.HasPrefix(v, "-") {
+ delete(t.TargetComponent, k)
+ }
+ }
+}
+func (p DeploymentPlan) FindLastTargetRole(target, role string) int {
+ for i := len(p.Steps) - 1; i >= 0; i-- {
+ if p.Steps[i].Role == role && p.Steps[i].Target == target {
+ return i
+ }
+ }
+ return -1
+}
+func (p DeploymentPlan) CanAppendToStep(step int, component ComponentSpec) bool {
+ canAppend := true
+ for _, d := range component.Dependencies {
+ resolved := false
+ for j := 0; j <= step; j++ {
+ for _, c := range p.Steps[j].Components {
+ if c.Component.Name == d && c.Action == "update" {
+ resolved = true
+ break
+ }
+ }
+ if resolved {
+ break
+ }
+ }
+ if !resolved {
+ return false
+ }
+ }
+ return canAppend
+}
+func (p DeploymentPlan) RevisedForDeletion() DeploymentPlan {
+ ret := DeploymentPlan{
+ Steps: make([]DeploymentStep, 0),
+ }
+ // create a stack to save deleted steps
+ deletedSteps := make([]DeploymentStep, 0)
+
+ for _, s := range p.Steps {
+ deleted := s.GetDeletedComponents()
+ all := s.GetComponents()
+ if len(deleted) == 0 {
+ ret.Steps = append(ret.Steps, s)
+ } else if len(deleted) == len(all) {
+ // add this step to the deleted steps stack
+ deletedSteps = append(deletedSteps, s)
+ } else {
+ //split the steps into two steps, one with updated only, one with deleted only
+ ret.Steps = append(ret.Steps, makeUpdateStep(s))
+ deletedSteps = append(deletedSteps, makeReversedDeletionStep(s))
+ }
+ }
+ for i := len(deletedSteps) - 1; i >= 0; i-- {
+ ret.Steps = append(ret.Steps, deletedSteps[i])
+ }
+ return ret
+}
+func makeUpdateStep(step DeploymentStep) DeploymentStep {
+ ret := DeploymentStep{
+ Target: step.Target,
+ Components: make([]ComponentStep, 0),
+ Role: step.Role,
+ IsFirst: step.IsFirst,
+ }
+ for _, c := range step.Components {
+ if c.Action == "update" {
+ ret.Components = append(ret.Components, c)
+ }
+ }
+ return ret
+}
+func makeReversedDeletionStep(step DeploymentStep) DeploymentStep {
+ ret := DeploymentStep{
+ Target: step.Target,
+ Components: make([]ComponentStep, 0),
+ Role: step.Role,
+ IsFirst: step.IsFirst,
+ }
+ for i := len(step.Components) - 1; i >= 0; i-- {
+ if step.Components[i].Action == "delete" {
+ ret.Components = append(ret.Components, step.Components[i])
+ }
+ }
+ return ret
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import "errors"
+
+// +kubebuilder:object:generate=true
+type RouteSpec struct {
+ Route string `json:"route"`
+ Type string `json:"type"`
+ Properties map[string]string `json:"properties,omitempty"`
+ Filters []FilterSpec `json:"filters,omitempty"`
+}
+
+func (c RouteSpec) DeepEquals(other IDeepEquals) (bool, error) { // avoid using reflect, which has performance problems
+ otherC, ok := other.(RouteSpec)
+ if !ok {
+ return false, errors.New("parameter is not a RouteSpec type")
+ }
+
+ if c.Route != otherC.Route {
+ return false, nil
+ }
+
+ if c.Type != otherC.Type {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Properties, otherC.Properties, nil) {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Filters, otherC.Filters) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+)
+
+type SiteState struct {
+ Id string `json:"id"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Spec *SiteSpec `json:"spec,omitempty"`
+ Status *SiteStatus `json:"status,omitempty"`
+}
+type TargetStatus struct {
+ State v1alpha2.State `json:"state,omitempty"`
+ Reason string `json:"reason,omitempty"`
+}
+type InstanceStatus struct {
+ State v1alpha2.State `json:"state,omitempty"`
+ Reason string `json:"reason,omitempty"`
+}
+
+// +kubebuilder:object:generate=true
+type SiteStatus struct {
+ IsOnline bool `json:"isOnline,omitempty"`
+ TargetStatuses map[string]TargetStatus `json:"targetStatuses,omitempty"`
+ InstanceStatuses map[string]InstanceStatus `json:"instanceStatuses,omitempty"`
+ LastReported string `json:"lastReported,omitempty"`
+}
+
+// +kubebuilder:object:generate=true
+type SiteSpec struct {
+ Name string `json:"name,omitempty"`
+ IsSelf bool `json:"isSelf,omitempty"`
+ PublicKey string `json:"secretHash,omitempty"`
+ Properties map[string]string `json:"properties,omitempty"`
+}
+
+func (s SiteSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherS, ok := other.(SiteSpec)
+ if !ok {
+ return false, errors.New("parameter is not a SiteSpec type")
+ }
+
+ if s.Name != otherS.Name {
+ return false, nil
+ }
+
+ if s.PublicKey != otherS.PublicKey {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+type SkillState struct {
+ Id string `json:"id"`
+ Spec *SkillSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:generate=true
+type SkillSpec struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Parameters map[string]string `json:"parameters,omitempty"`
+ Nodes []NodeSpec `json:"nodes"`
+ Properties map[string]string `json:"properties,omitempty"`
+ Bindings []BindingSpec `json:"bindings,omitempty"`
+ Edges []EdgeSpec `json:"edges"`
+}
+
+// +kubebuilder:object:generate=true
+type SkillPackageSpec struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Skill string `json:"skill"`
+ Properties map[string]string `json:"properties,omitempty"`
+ Constraints string `json:"constraints,omitempty"`
+ Routes []RouteSpec `json:"routes,omitempty"`
+}
+
+func (c SkillSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherSkillSpec, ok := other.(*SkillSpec)
+ if !ok {
+ return false, nil
+ }
+ if c.DisplayName != otherSkillSpec.DisplayName {
+ return false, nil
+ }
+ if !StringMapsEqual(c.Parameters, otherSkillSpec.Parameters, nil) {
+ return false, nil
+ }
+ if !SlicesEqual(c.Nodes, otherSkillSpec.Nodes) {
+ return false, nil
+ }
+ if !StringMapsEqual(c.Properties, otherSkillSpec.Properties, nil) {
+ return false, nil
+ }
+ if !SlicesEqual(c.Bindings, otherSkillSpec.Bindings) {
+ return false, nil
+ }
+ if !SlicesEqual(c.Edges, otherSkillSpec.Edges) {
+ return false, nil
+ }
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+)
+
+type (
+ SolutionState struct {
+ Id string `json:"id"`
+ Scope string `json:"scope"`
+ Spec *SolutionSpec `json:"spec,omitempty"`
+ }
+
+ SolutionSpec struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Scope string `json:"scope,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Components []ComponentSpec `json:"components,omitempty"`
+ }
+)
+
+func (c SolutionSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(SolutionSpec)
+ if !ok {
+ return false, errors.New("parameter is not a SolutionSpec type")
+ }
+
+ if c.DisplayName != otherC.DisplayName {
+ return false, nil
+ }
+
+ if c.Scope != otherC.Scope {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Metadata, otherC.Metadata, nil) {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Components, otherC.Components) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "time"
+
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+)
+
+type ComponentResultSpec struct {
+ Status v1alpha2.State `json:"status"`
+ Message string `json:"message"`
+}
+type TargetResultSpec struct {
+ Status string `json:"status"`
+ Message string `json:"message,omitempty"`
+ ComponentResults map[string]ComponentResultSpec `json:"components,omitempty"`
+}
+type SummarySpec struct {
+ TargetCount int `json:"targetCount"`
+ SuccessCount int `json:"successCount"`
+ TargetResults map[string]TargetResultSpec `json:"targets,omitempty"`
+ SummaryMessage string `json:"message,omitempty"`
+ Skipped bool `json:"skipped"`
+ IsRemoval bool `json:"isRemoval"`
+}
+type SummaryResult struct {
+ Summary SummarySpec `json:"summary"`
+ Generation string `json:"generation"`
+ Time time.Time `json:"time"`
+}
+
+func (s *SummarySpec) UpdateTargetResult(target string, spec TargetResultSpec) {
+ s.TargetResults[target] = spec
+ count := 0
+ for _, r := range s.TargetResults {
+ if r.Status == "OK" {
+ count++
+ }
+ }
+ s.SuccessCount = count
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import "errors"
+
+type (
+ // TargetState defines the current state of the target
+ TargetState struct {
+ Id string `json:"id"`
+ Scope string `json:"scope,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Status map[string]string `json:"status,omitempty"`
+ Spec *TargetSpec `json:"spec,omitempty"`
+ }
+
+ // TargetSpec defines the spec property of the TargetState
+ TargetSpec struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Scope string `json:"scope,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Properties map[string]string `json:"properties,omitempty"`
+ Components []ComponentSpec `json:"components,omitempty"`
+ Constraints string `json:"constraints,omitempty"`
+ Topologies []TopologySpec `json:"topologies,omitempty"`
+ ForceRedeploy bool `json:"forceRedeploy,omitempty"`
+ Generation string `json:"generation,omitempty"`
+ // Defines the version of a particular resource
+ Version string `json:"version,omitempty"`
+ }
+)
+
+func (c TargetSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(TargetSpec)
+ if !ok {
+ return false, errors.New("parameter is not a TargetSpec type")
+ }
+
+ if c.DisplayName != otherC.DisplayName {
+ return false, nil
+ }
+
+ if c.Scope != otherC.Scope {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Metadata, otherC.Metadata, nil) {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Properties, otherC.Properties, nil) {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Components, otherC.Components) {
+ return false, nil
+ }
+
+ if c.Constraints != otherC.Constraints {
+ return false, nil
+ }
+
+ if !SlicesEqual(c.Topologies, otherC.Topologies) {
+ return false, nil
+ }
+
+ if c.ForceRedeploy != otherC.ForceRedeploy {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "fmt"
+ "strings"
+
+ go_slices "golang.org/x/exp/slices"
+ "helm.sh/helm/v3/pkg/strvals"
+)
+
+type (
+ // IDeepEquals interface defines an interface for memberwise equality comparision
+ IDeepEquals interface {
+ DeepEquals(other IDeepEquals) (bool, error)
+ }
+
+ ValueInjections struct {
+ InstanceId string
+ SolutionId string
+ TargetId string
+ ActivationId string
+ CatalogId string
+ CampaignId string
+ DeviceId string
+ SkillId string
+ ModelId string
+ SiteId string
+ }
+)
+
+// stringMapsEqual compares two string maps for equality
+func StringMapsEqual(a map[string]string, b map[string]string, ignoredMissingKeys []string) bool {
+ // if len(a) != len(b) {
+ // return false
+ // }
+
+ //TODO: I don't think we need this anymore
+
+ for k, v := range a {
+ if bv, ok := b[k]; ok {
+ if bv != v {
+ if !strings.Contains(bv, "${{$instance()}}") &&
+ !strings.Contains(v, "${{$instance()}}") &&
+ !strings.Contains(bv, "${{$solution()}}") &&
+ !strings.Contains(v, "${{$solution()}}") &&
+ !strings.Contains(bv, "${{$target()}}") &&
+ !strings.Contains(v, "${{$target()}}") { // Skip comparision because $instance is filled by different instances
+ return false
+ }
+ }
+ } else {
+ if !go_slices.Contains(ignoredMissingKeys, k) {
+ return false
+ }
+ }
+ }
+
+ for k, v := range b {
+ if bv, ok := a[k]; ok {
+ if bv != v {
+ if !strings.Contains(bv, "${{$instance()}}") &&
+ !strings.Contains(v, "${{$instance()}}") &&
+ !strings.Contains(bv, "${{$solution()}}") &&
+ !strings.Contains(v, "${{$solution()}}") &&
+ !strings.Contains(bv, "${{$target()}}") &&
+ !strings.Contains(v, "${{$target()}}") { // Skip comparision because $instance is filled by different instances
+ return false
+ }
+ }
+ } else {
+ if !go_slices.Contains(ignoredMissingKeys, k) {
+ return false
+ }
+ }
+ }
+
+ return true
+}
+
+func StringStringMapsEqual(a map[string]map[string]string, b map[string]map[string]string, ignoredMissingKeys []string) bool {
+ for k, v := range a {
+ if bv, ok := b[k]; ok {
+ if !StringMapsEqual(v, bv, ignoredMissingKeys) {
+ return false
+ }
+ } else {
+ if !go_slices.Contains(ignoredMissingKeys, k) {
+ return false
+ }
+ }
+ }
+
+ for k, v := range b {
+ if bv, ok := a[k]; ok {
+ if !StringMapsEqual(v, bv, ignoredMissingKeys) {
+ return false
+ }
+ } else {
+ if !go_slices.Contains(ignoredMissingKeys, k) {
+ return false
+ }
+ }
+ }
+
+ return true
+}
+
+// Compatibility Helper
+func ExtractRawEnvFromProperties(properties map[string]interface{}) map[string]string {
+ env := make(map[string]string)
+ for k, v := range properties {
+ if strings.HasPrefix(k, "env.") {
+ env[k] = fmt.Sprintf("%v", v)
+ }
+ }
+
+ return env
+}
+
+func EnvMapsEqual(a map[string]string, b map[string]string) bool {
+ // if len(a) != len(b) {
+ // return false
+ // }
+
+ for k, v := range a {
+ if strings.HasPrefix(k, "env.") {
+ if bv, ok := b[k]; ok {
+ if bv != v {
+ if !strings.Contains(bv, "${{$instance()}}") &&
+ !strings.Contains(v, "${{$instance()}}") &&
+ !strings.Contains(bv, "${{$solution()}}") &&
+ !strings.Contains(v, "${{$solution()}}") &&
+ !strings.Contains(bv, "${{$target()}}") &&
+ !strings.Contains(v, "${{$target()}}") { // Skip comparision because $instance is filled by different instances
+ return false
+ }
+ }
+ }
+ }
+ }
+
+ for k, v := range b {
+ if strings.HasPrefix(k, "env.") {
+ if bv, ok := a[k]; ok {
+ if bv != v {
+ if !strings.Contains(bv, "${{$instance()}}") &&
+ !strings.Contains(v, "${{$instance()}}") &&
+ !strings.Contains(bv, "${{$solution()}}") &&
+ !strings.Contains(v, "${{$solution()}}") &&
+ !strings.Contains(bv, "${{$target()}}") &&
+ !strings.Contains(v, "${{$target()}}") { // Skip comparision because $instance is filled by different instances
+ return false
+ }
+ }
+ }
+ }
+ }
+
+ return true
+}
+
+// SliceEuql compares two slices of IDeepEqual items, ignoring the order of items
+// It returns two if the two slices are exactly the same, otherwise it returns false
+func SlicesEqual[K IDeepEquals](a []K, b []K) bool {
+ if len(a) != len(b) {
+ return false
+ }
+
+ used := make(map[int]bool)
+ for _, ia := range a {
+ found := false
+ for j, jb := range b {
+ if _, ok := used[j]; !ok {
+ t, e := ia.DeepEquals(jb)
+ if e != nil {
+ return false
+ }
+
+ if t {
+ used[j] = true
+ found = true
+ break
+ }
+ }
+ }
+
+ if !found {
+ return false
+ }
+ }
+
+ return true
+}
+
+func SlicesCover[K IDeepEquals](src []K, dest []K) bool {
+ for _, ia := range src {
+ found := false
+ for _, jb := range dest {
+ t, e := ia.DeepEquals(jb)
+ if e == nil && t {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ return false
+ }
+ }
+
+ return true
+}
+
+func SlicesAny[K IDeepEquals](src []K, dest []K) bool {
+ for _, ia := range src {
+ for _, jb := range dest {
+ t, e := ia.DeepEquals(jb)
+ if e == nil && t {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func CheckProperty(a map[string]string, b map[string]string, key string, ignoreCase bool) bool {
+ if va, oka := a[key]; oka {
+ if vb, okb := b[key]; okb {
+ if ignoreCase {
+ return strings.EqualFold(va, vb)
+ } else {
+ return va == vb
+ }
+ }
+
+ return false
+ }
+
+ return true
+}
+
+func CheckPropertyCompat(a map[string]interface{}, b map[string]interface{}, key string, ignoreCase bool) bool {
+ if va, oka := a[key]; oka {
+ if vb, okb := b[key]; okb {
+ if ignoreCase {
+ return strings.EqualFold(fmt.Sprintf("%v", va), fmt.Sprintf("%v", vb))
+ } else {
+ return va == vb
+ }
+ }
+
+ return false
+ }
+
+ return true
+}
+
+func HasSameProperty(a map[string]string, b map[string]string, key string) bool {
+ va, oka := a[key]
+ vb, okb := b[key]
+ if oka && okb {
+ return va == vb
+ } else if !oka && !okb {
+ return true
+ } else {
+ return false
+ }
+
+}
+
+func HasSamePropertyCompat(a map[string]interface{}, b map[string]interface{}, key string) bool {
+ va, oka := a[key]
+ vb, okb := b[key]
+ if oka && okb {
+ return fmt.Sprintf("%v", va) == fmt.Sprintf("%v", vb)
+ } else if !oka && !okb {
+ return true
+ } else {
+ return false
+ }
+
+}
+
+func CollectPropertiesWithPrefix(col map[string]interface{}, prefix string, injections *ValueInjections, withHierarchy bool) map[string]interface{} {
+ ret := make(map[string]interface{})
+ for k, v := range col {
+ if v, ok := v.(string); ok && strings.HasPrefix(k, prefix) {
+ key := k[len(prefix):]
+ if withHierarchy {
+ strvals.ParseInto(fmt.Sprintf("%s=%s", key, ResolveString(v, injections)), ret)
+ } else {
+ ret[key] = ResolveString(v, injections)
+ }
+ }
+ }
+
+ return ret
+}
+
+func ReadPropertyCompat(col map[string]interface{}, key string, injections *ValueInjections) string {
+ if v, ok := col[key]; ok {
+ return ResolveString(fmt.Sprintf("%v", v), injections)
+ }
+
+ return ""
+}
+
+func ReadProperty(col map[string]string, key string, injections *ValueInjections) string {
+ if v, ok := col[key]; ok {
+ return ResolveString(v, injections)
+ }
+
+ return ""
+}
+
+func ResolveString(value string, injections *ValueInjections) string {
+ //TODO: future enhancement - analyze the syntax instead of doing simply string replacement
+ if injections != nil {
+ value = strings.ReplaceAll(value, "${{$instance()}}", injections.InstanceId)
+ value = strings.ReplaceAll(value, "${{$solution()}}", injections.SolutionId)
+ value = strings.ReplaceAll(value, "${{$target()}}", injections.TargetId)
+ value = strings.ReplaceAll(value, "${{$activation()}}", injections.ActivationId)
+ value = strings.ReplaceAll(value, "${{$catalog()}}", injections.CatalogId)
+ value = strings.ReplaceAll(value, "${{$campaign()}}", injections.CampaignId)
+ value = strings.ReplaceAll(value, "${{$device()}}", injections.DeviceId)
+ value = strings.ReplaceAll(value, "${{$model()}}", injections.ModelId)
+ value = strings.ReplaceAll(value, "${{$skill()}}", injections.SkillId)
+ value = strings.ReplaceAll(value, "${{$site()}}", injections.SiteId)
+ }
+
+ return value
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+)
+
+type PropertyDesc struct {
+ Name string `json:"name"`
+ IgnoreCase bool `json:"ignoreCase,omitempty"`
+ SkipIfMissing bool `json:"skipIfMissing,omitempty"`
+ PrefixMatch bool `json:"prefixMatch,omitempty"`
+ IsComponentName bool `json:"isComponentName,omitempty"`
+}
+type ValidationRule struct {
+ RequiredComponentType string `json:"requiredType"`
+ ChangeDetectionProperties []PropertyDesc `json:"changeDetection,omitempty"`
+ ChangeDetectionMetadata []PropertyDesc `json:"changeDetectionMetadata,omitempty"`
+ RequiredProperties []string `json:"requiredProperties"`
+ OptionalProperties []string `json:"optionalProperties"`
+ RequiredMetadata []string `json:"requiredMetadata"`
+ OptionalMetadata []string `json:"optionalMetadata"`
+ // a provider that supports scope isolation can deploy to specified scopes other than the "default" scope.
+ // instances from different scopes are isolated from each other.
+ ScopeIsolation bool `json:"supportScopes,omitempty"`
+ // a provider that supports instance isolation can deploy multiple instances on the same target without conflicts.
+ InstanceIsolation bool `json:"instanceIsolation,omitempty"`
+}
+
+func (v ValidationRule) ValidateInputs(inputs map[string]interface{}) error {
+ // required properties must all present
+ for _, p := range v.RequiredProperties {
+ if ReadPropertyCompat(inputs, p, nil) == "" {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("required property '%s' is missing", p), v1alpha2.BadRequest)
+ }
+ }
+ return nil
+}
+
+func (v ValidationRule) Validate(components []ComponentSpec) error {
+ for _, c := range components {
+ err := v.validateComponent(c)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (v ValidationRule) IsComponentChanged(old ComponentSpec, new ComponentSpec) bool {
+ for _, c := range v.ChangeDetectionProperties {
+ if strings.Contains(c.Name, "*") {
+ escapedPattern := regexp.QuoteMeta(c.Name)
+ // Replace the wildcard (*) with a regular expression pattern
+ regexpPattern := strings.ReplaceAll(escapedPattern, `\*`, ".*")
+ // Compile the regular expression
+ regexpObject := regexp.MustCompile("^" + regexpPattern + "$")
+ for k := range old.Properties {
+ if regexpObject.MatchString(k) {
+ if compareProperties(c, old, new, k) {
+ return true
+ }
+ }
+ }
+ } else {
+ if c.IsComponentName {
+ if !compareStrings(old.Name, new.Name, c.IgnoreCase, c.SkipIfMissing) {
+ return true
+ }
+ } else {
+ if compareProperties(c, old, new, c.Name) {
+ return true
+ }
+ }
+ }
+ }
+ for _, c := range v.ChangeDetectionMetadata {
+ if strings.Contains(c.Name, "*") {
+ escapedPattern := regexp.QuoteMeta(c.Name)
+ // Replace the wildcard (*) with a regular expression pattern
+ regexpPattern := strings.ReplaceAll(escapedPattern, `\*`, ".*")
+ // Compile the regular expression
+ regexpObject := regexp.MustCompile("^" + regexpPattern + "$")
+ for k := range old.Metadata {
+ if regexpObject.MatchString(k) {
+ if compareMetadata(c, old, new, k) {
+ return true
+ }
+ }
+ }
+ } else {
+ if compareMetadata(c, old, new, c.Name) {
+ return true
+ }
+ }
+ }
+ return false
+}
+func compareStrings(a, b string, ignoreCase bool, prefixMatch bool) bool {
+ ta := a
+ tb := b
+ if ignoreCase {
+ ta = strings.ToLower(a)
+ tb = strings.ToLower(b)
+ }
+ if !prefixMatch {
+ return ta == tb
+ } else {
+ return strings.HasPrefix(tb, ta)
+ }
+}
+func compareProperties(c PropertyDesc, old ComponentSpec, new ComponentSpec, key string) bool {
+ if v, ok := old.Properties[key]; ok {
+ if nv, nok := new.Properties[key]; nok {
+ if !compareStrings(fmt.Sprintf("%v", v), fmt.Sprintf("%v", nv), c.IgnoreCase, c.PrefixMatch) {
+ return true
+ }
+ }
+ } else {
+ if !c.SkipIfMissing {
+ return true
+ }
+ }
+ return false
+}
+
+func compareMetadata(c PropertyDesc, old ComponentSpec, new ComponentSpec, key string) bool {
+ if v, ok := old.Metadata[key]; ok {
+ if nv, nok := new.Metadata[key]; nok {
+ if !compareStrings(fmt.Sprintf("%v", v), fmt.Sprintf("%v", nv), c.IgnoreCase, c.PrefixMatch) {
+ return true
+ }
+ }
+ } else {
+ if !c.SkipIfMissing {
+ return true
+ }
+ }
+ return false
+}
+
+func (v ValidationRule) validateComponent(component ComponentSpec) error {
+ //required component type must be set
+ if v.RequiredComponentType != "" && v.RequiredComponentType != component.Type {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("provider requires component type '%s', but '%s' is found instead", v.RequiredComponentType, component.Type), v1alpha2.BadRequest)
+ }
+
+ // required properties must all present
+ for _, p := range v.RequiredProperties {
+ if ReadPropertyCompat(component.Properties, p, nil) == "" {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("required property '%s' is missing", p), v1alpha2.BadRequest)
+ }
+ }
+
+ // required metadata must all present
+ for _, p := range v.RequiredMetadata {
+ if ReadProperty(component.Metadata, p, nil) == "" {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("required metadata '%s' is missing", p), v1alpha2.BadRequest)
+ }
+ }
+
+ return nil
+}
+
+
+
//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package model
+
+import ()
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *BindingSpec) DeepCopyInto(out *BindingSpec) {
+ *out = *in
+ if in.Config != nil {
+ in, out := &in.Config, &out.Config
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindingSpec.
+func (in *BindingSpec) DeepCopy() *BindingSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(BindingSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ComponentError) DeepCopyInto(out *ComponentError) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentError.
+func (in *ComponentError) DeepCopy() *ComponentError {
+ if in == nil {
+ return nil
+ }
+ out := new(ComponentError)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DeviceSpec) DeepCopyInto(out *DeviceSpec) {
+ *out = *in
+ if in.Properties != nil {
+ in, out := &in.Properties, &out.Properties
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Bindings != nil {
+ in, out := &in.Bindings, &out.Bindings
+ *out = make([]BindingSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceSpec.
+func (in *DeviceSpec) DeepCopy() *DeviceSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(DeviceSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ErrorType) DeepCopyInto(out *ErrorType) {
+ *out = *in
+ if in.Details != nil {
+ in, out := &in.Details, &out.Details
+ *out = make([]TargetError, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ErrorType.
+func (in *ErrorType) DeepCopy() *ErrorType {
+ if in == nil {
+ return nil
+ }
+ out := new(ErrorType)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FilterSpec) DeepCopyInto(out *FilterSpec) {
+ *out = *in
+ if in.Parameters != nil {
+ in, out := &in.Parameters, &out.Parameters
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FilterSpec.
+func (in *FilterSpec) DeepCopy() *FilterSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(FilterSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InstanceSpec) DeepCopyInto(out *InstanceSpec) {
+ *out = *in
+ if in.Parameters != nil {
+ in, out := &in.Parameters, &out.Parameters
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Metadata != nil {
+ in, out := &in.Metadata, &out.Metadata
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ in.Target.DeepCopyInto(&out.Target)
+ if in.Topologies != nil {
+ in, out := &in.Topologies, &out.Topologies
+ *out = make([]TopologySpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Pipelines != nil {
+ in, out := &in.Pipelines, &out.Pipelines
+ *out = make([]PipelineSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Arguments != nil {
+ in, out := &in.Arguments, &out.Arguments
+ *out = make(map[string]map[string]string, len(*in))
+ for key, val := range *in {
+ var outVal map[string]string
+ if val == nil {
+ (*out)[key] = nil
+ } else {
+ in, out := &val, &outVal
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ (*out)[key] = outVal
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceSpec.
+func (in *InstanceSpec) DeepCopy() *InstanceSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(InstanceSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ModelSpec) DeepCopyInto(out *ModelSpec) {
+ *out = *in
+ if in.Properties != nil {
+ in, out := &in.Properties, &out.Properties
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Bindings != nil {
+ in, out := &in.Bindings, &out.Bindings
+ *out = make([]BindingSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ModelSpec.
+func (in *ModelSpec) DeepCopy() *ModelSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(ModelSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeSpec) DeepCopyInto(out *NodeSpec) {
+ *out = *in
+ if in.Configurations != nil {
+ in, out := &in.Configurations, &out.Configurations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Inputs != nil {
+ in, out := &in.Inputs, &out.Inputs
+ *out = make([]RouteSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Outputs != nil {
+ in, out := &in.Outputs, &out.Outputs
+ *out = make([]RouteSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeSpec.
+func (in *NodeSpec) DeepCopy() *NodeSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ObjectRef) DeepCopyInto(out *ObjectRef) {
+ *out = *in
+ if in.Metadata != nil {
+ in, out := &in.Metadata, &out.Metadata
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObjectRef.
+func (in *ObjectRef) DeepCopy() *ObjectRef {
+ if in == nil {
+ return nil
+ }
+ out := new(ObjectRef)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PipelineSpec) DeepCopyInto(out *PipelineSpec) {
+ *out = *in
+ if in.Parameters != nil {
+ in, out := &in.Parameters, &out.Parameters
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PipelineSpec.
+func (in *PipelineSpec) DeepCopy() *PipelineSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(PipelineSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ProvisioningStatus) DeepCopyInto(out *ProvisioningStatus) {
+ *out = *in
+ in.Error.DeepCopyInto(&out.Error)
+ if in.Output != nil {
+ in, out := &in.Output, &out.Output
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvisioningStatus.
+func (in *ProvisioningStatus) DeepCopy() *ProvisioningStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(ProvisioningStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RouteSpec) DeepCopyInto(out *RouteSpec) {
+ *out = *in
+ if in.Properties != nil {
+ in, out := &in.Properties, &out.Properties
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Filters != nil {
+ in, out := &in.Filters, &out.Filters
+ *out = make([]FilterSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RouteSpec.
+func (in *RouteSpec) DeepCopy() *RouteSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(RouteSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SiteSpec) DeepCopyInto(out *SiteSpec) {
+ *out = *in
+ if in.Properties != nil {
+ in, out := &in.Properties, &out.Properties
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SiteSpec.
+func (in *SiteSpec) DeepCopy() *SiteSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(SiteSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SiteStatus) DeepCopyInto(out *SiteStatus) {
+ *out = *in
+ if in.TargetStatuses != nil {
+ in, out := &in.TargetStatuses, &out.TargetStatuses
+ *out = make(map[string]TargetStatus, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.InstanceStatuses != nil {
+ in, out := &in.InstanceStatuses, &out.InstanceStatuses
+ *out = make(map[string]InstanceStatus, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SiteStatus.
+func (in *SiteStatus) DeepCopy() *SiteStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(SiteStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SkillPackageSpec) DeepCopyInto(out *SkillPackageSpec) {
+ *out = *in
+ if in.Properties != nil {
+ in, out := &in.Properties, &out.Properties
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Routes != nil {
+ in, out := &in.Routes, &out.Routes
+ *out = make([]RouteSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillPackageSpec.
+func (in *SkillPackageSpec) DeepCopy() *SkillPackageSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(SkillPackageSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SkillSpec) DeepCopyInto(out *SkillSpec) {
+ *out = *in
+ if in.Parameters != nil {
+ in, out := &in.Parameters, &out.Parameters
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Nodes != nil {
+ in, out := &in.Nodes, &out.Nodes
+ *out = make([]NodeSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Properties != nil {
+ in, out := &in.Properties, &out.Properties
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Bindings != nil {
+ in, out := &in.Bindings, &out.Bindings
+ *out = make([]BindingSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.Edges != nil {
+ in, out := &in.Edges, &out.Edges
+ *out = make([]EdgeSpec, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SkillSpec.
+func (in *SkillSpec) DeepCopy() *SkillSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(SkillSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TargetError) DeepCopyInto(out *TargetError) {
+ *out = *in
+ if in.Details != nil {
+ in, out := &in.Details, &out.Details
+ *out = make([]ComponentError, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetError.
+func (in *TargetError) DeepCopy() *TargetError {
+ if in == nil {
+ return nil
+ }
+ out := new(TargetError)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TargetSelector) DeepCopyInto(out *TargetSelector) {
+ *out = *in
+ if in.Selector != nil {
+ in, out := &in.Selector, &out.Selector
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TargetSelector.
+func (in *TargetSelector) DeepCopy() *TargetSelector {
+ if in == nil {
+ return nil
+ }
+ out := new(TargetSelector)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TopologySpec) DeepCopyInto(out *TopologySpec) {
+ *out = *in
+ if in.Selector != nil {
+ in, out := &in.Selector, &out.Selector
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ if in.Bindings != nil {
+ in, out := &in.Bindings, &out.Bindings
+ *out = make([]BindingSpec, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TopologySpec.
+func (in *TopologySpec) DeepCopy() *TopologySpec {
+ if in == nil {
+ return nil
+ }
+ out := new(TopologySpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package catalog
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sync"
+
+ "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/contexts"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
+ coa_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/utils"
+)
+
+var msLock sync.Mutex
+
+type CatalogConfigProviderConfig struct {
+ BaseUrl string `json:"baseUrl"`
+ User string `json:"user"`
+ Password string `json:"password"`
+}
+
+type CatalogConfigProvider struct {
+ Config CatalogConfigProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func (s *CatalogConfigProvider) Init(config providers.IProviderConfig) error {
+ msLock.Lock()
+ defer msLock.Unlock()
+ mockConfig, err := toCatalogConfigProviderConfig(config)
+ if err != nil {
+ return err
+ }
+ s.Config = mockConfig
+ return nil
+}
+func (s *CatalogConfigProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func toCatalogConfigProviderConfig(config providers.IProviderConfig) (CatalogConfigProviderConfig, error) {
+ ret := CatalogConfigProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *CatalogConfigProvider) InitWithMap(properties map[string]string) error {
+ config, err := CatalogConfigProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func CatalogConfigProviderConfigFromMap(properties map[string]string) (CatalogConfigProviderConfig, error) {
+ ret := CatalogConfigProviderConfig{}
+ baseUrl, err := utils.GetString(properties, "baseUrl")
+ if err != nil {
+ return ret, err
+ }
+ ret.BaseUrl = baseUrl
+ if ret.BaseUrl == "" {
+ return ret, v1alpha2.NewCOAError(nil, "baseUrl is required", v1alpha2.BadConfig)
+ }
+ user, err := utils.GetString(properties, "user")
+ if err != nil {
+ return ret, err
+ }
+ ret.User = user
+ if ret.User == "" {
+ return ret, v1alpha2.NewCOAError(nil, "user is required", v1alpha2.BadConfig)
+ }
+ password, err := utils.GetString(properties, "password")
+ if err != nil {
+ return ret, err
+ }
+ ret.Password = password
+ return ret, nil
+}
+func (m *CatalogConfigProvider) unwindOverrides(override string, field string) (string, error) {
+ catalog, err := utils.GetCatalog(context.TODO(), m.Config.BaseUrl, override, m.Config.User, m.Config.Password)
+ if err != nil {
+ return "", err
+ }
+ if v, ok := catalog.Spec.Properties[field]; ok {
+ return v.(string), nil
+ }
+ if catalog.Spec.ParentName != "" {
+ return m.unwindOverrides(catalog.Spec.ParentName, field)
+ }
+ return "", v1alpha2.NewCOAError(nil, fmt.Sprintf("field '%s' is not found in configuration '%s'", field, override), v1alpha2.NotFound)
+}
+func (m *CatalogConfigProvider) Read(object string, field string, localcontext interface{}) (interface{}, error) {
+ catalog, err := utils.GetCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password)
+ if err != nil {
+ return "", err
+ }
+
+ if v, ok := catalog.Spec.Properties[field]; ok {
+ return m.traceValue(v, localcontext)
+ }
+
+ if catalog.Spec.ParentName != "" {
+ overrid, err := m.unwindOverrides(catalog.Spec.ParentName, field)
+ if err != nil {
+ return "", err
+ } else {
+ return overrid, nil
+ }
+ }
+
+ return "", v1alpha2.NewCOAError(nil, fmt.Sprintf("field '%s' is not found in configuration '%s'", field, object), v1alpha2.NotFound)
+}
+func (m *CatalogConfigProvider) ReadObject(object string, localcontext interface{}) (map[string]interface{}, error) {
+ catalog, err := utils.GetCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password)
+ if err != nil {
+ return nil, err
+ }
+ ret := map[string]interface{}{}
+ for k, v := range catalog.Spec.Properties {
+ tv, err := m.traceValue(v, localcontext)
+ if err != nil {
+ return nil, err
+ }
+ // line 189-196 extracts the returned map and merge the keys with the parent
+ // this allows a referenced configuration to be overriden by local values
+ if tmap, ok := tv.(map[string]interface{}); ok {
+ for tk, tv := range tmap {
+ if _, ok := ret[tk]; !ok {
+ ret[tk] = tv
+ }
+ }
+ continue
+ }
+ ret[k] = tv
+ }
+ return ret, nil
+}
+func (m *CatalogConfigProvider) traceValue(v interface{}, localcontext interface{}) (interface{}, error) {
+ switch val := v.(type) {
+ case string:
+ parser := utils.NewParser(val)
+ context := m.Context.VencorContext.EvaluationContext.Clone()
+ context.DeploymentSpec = m.Context.VencorContext.EvaluationContext.DeploymentSpec
+ if localcontext != nil {
+ if ltx, ok := localcontext.(coa_utils.EvaluationContext); ok {
+ context.Inputs = ltx.Inputs
+ context.Outputs = ltx.Outputs
+ context.Value = ltx.Value
+ context.Properties = ltx.Properties
+ context.Component = ltx.Component
+ if ltx.DeploymentSpec != nil {
+ context.DeploymentSpec = ltx.DeploymentSpec
+ }
+ }
+ }
+ v, err := parser.Eval(*context)
+ if err != nil {
+ return "", err
+ }
+ switch vt := v.(type) {
+ case string:
+ return vt, nil
+ default:
+ return m.traceValue(v, localcontext)
+ }
+ case []interface{}:
+ ret := []interface{}{}
+ for _, v := range val {
+ tv, err := m.traceValue(v, localcontext)
+ if err != nil {
+ return "", err
+ }
+ ret = append(ret, tv)
+ }
+ return ret, nil
+ case map[string]interface{}:
+ ret := map[string]interface{}{}
+ for k, v := range val {
+ tv, err := m.traceValue(v, localcontext)
+ if err != nil {
+ return "", err
+ }
+ ret[k] = tv
+ }
+ return ret, nil
+ default:
+ return val, nil
+ }
+}
+func (m *CatalogConfigProvider) Set(object string, field string, value interface{}) error {
+ catalog, err := utils.GetCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password)
+ if err != nil {
+ return err
+ }
+ catalog.Spec.Properties[field] = value
+ data, _ := json.Marshal(catalog.Spec)
+ return utils.UpsertCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password, data)
+}
+func (m *CatalogConfigProvider) SetObject(object string, value map[string]interface{}) error {
+ catalog, err := utils.GetCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password)
+ if err != nil {
+ return err
+ }
+ catalog.Spec.Properties = map[string]interface{}{}
+ for k, v := range value {
+ catalog.Spec.Properties[k] = v
+ }
+ data, _ := json.Marshal(catalog.Spec)
+ return utils.UpsertCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password, data)
+}
+func (m *CatalogConfigProvider) Remove(object string, field string) error {
+ catlog, err := utils.GetCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password)
+ if err != nil {
+ return err
+ }
+ if _, ok := catlog.Spec.Properties[field]; !ok {
+ return v1alpha2.NewCOAError(nil, "field not found", v1alpha2.NotFound)
+ }
+ delete(catlog.Spec.Properties, field)
+ data, _ := json.Marshal(catlog.Spec)
+ return utils.UpsertCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password, data)
+}
+func (m *CatalogConfigProvider) RemoveObject(object string) error {
+ return utils.DeleteCatalog(context.TODO(), m.Config.BaseUrl, object, m.Config.User, m.Config.Password)
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package memorygraph
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/graph"
+ "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"
+)
+
+type MemoryGraphProviderConfig struct {
+}
+
+type MemoryGraphProvider struct {
+ Config MemoryGraphProviderConfig
+ Context *contexts.ManagerContext
+ Data []v1alpha2.INode
+}
+
+func (g *MemoryGraphProvider) Init(config providers.IProviderConfig) error {
+ mockConfig, err := toMemoryGraphProviderConfig(config)
+ if err != nil {
+ return err
+ }
+ g.Config = mockConfig
+ return nil
+}
+func (s *MemoryGraphProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func toMemoryGraphProviderConfig(config providers.IProviderConfig) (MemoryGraphProviderConfig, error) {
+ ret := MemoryGraphProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *MemoryGraphProvider) InitWithMap(properties map[string]string) error {
+ config, err := MemoryhGraphProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func MemoryhGraphProviderConfigFromMap(properties map[string]string) (MemoryGraphProviderConfig, error) {
+ ret := MemoryGraphProviderConfig{}
+ return ret, nil
+}
+func (i *MemoryGraphProvider) GetSet(ctx context.Context, request graph.GetRequest) (graph.GetSetResponse, error) {
+ ctx, span := observability.StartSpan("Memory Graph Provider", ctx, &map[string]string{
+ "method": "GetSet",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ ret := graph.GetSetResponse{
+ Nodes: make([]v1alpha2.INode, 0),
+ }
+ _, err = i.getNode(request.Name, request.Filter)
+ if err != nil {
+ return ret, err
+ }
+ for _, node := range i.Data {
+ if request.Filter != "" && node.GetType() != request.Filter {
+ continue
+ }
+ if node.GetParent() == request.Name {
+ ret.Nodes = append(ret.Nodes, node)
+ }
+ }
+ return ret, nil
+}
+func (i *MemoryGraphProvider) GetTree(ctx context.Context, request graph.GetRequest) (graph.GetSetResponse, error) {
+ ctx, span := observability.StartSpan("Memory Graph Provider", ctx, &map[string]string{
+ "method": "GetTree",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ ret := graph.GetSetResponse{
+ Nodes: make([]v1alpha2.INode, 0),
+ }
+ root, err := i.getNode(request.Name, request.Filter)
+ if err != nil {
+ return ret, err
+ }
+ ret.Nodes = append(ret.Nodes, root)
+ i.collectChildren(root, request.Filter, &ret)
+ return ret, nil
+}
+func (i *MemoryGraphProvider) collectChildren(root v1alpha2.INode, filter string, ret *graph.GetSetResponse) {
+ queue := []v1alpha2.INode{root}
+ for len(queue) > 0 {
+ node := queue[0]
+ queue = queue[1:]
+ for _, child := range i.Data {
+ if filter != "" && child.GetType() != filter {
+ continue
+ }
+ if child.GetParent() == node.GetId() {
+ ret.Nodes = append(ret.Nodes, child)
+ queue = append(queue, child)
+ }
+ }
+ }
+}
+func (i *MemoryGraphProvider) GetGraph(ctx context.Context, request graph.GetRequest) (graph.GetGraphResponse, error) {
+ return graph.GetGraphResponse{}, v1alpha2.NewCOAError(nil, "not implemented", v1alpha2.NotImplemented)
+}
+func (i *MemoryGraphProvider) getNode(name string, filter string) (v1alpha2.INode, error) {
+ var root v1alpha2.INode
+ for _, node := range i.Data {
+ if filter != "" && node.GetType() != filter {
+ continue
+ }
+ if node.GetId() == name {
+ root = node
+ break
+ }
+ }
+ if root == nil {
+ return nil, v1alpha2.NewCOAError(nil, "root node not found", v1alpha2.NotFound)
+ }
+ return root, nil
+}
+func (i *MemoryGraphProvider) GetChain(ctx context.Context, request graph.GetRequest) (graph.GetSetResponse, error) {
+ ctx, span := observability.StartSpan("Memory Graph Provider", ctx, &map[string]string{
+ "method": "GetChain",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ rep, err := i.GetTree(ctx, request)
+ return rep, err
+}
+func (i *MemoryGraphProvider) GetSets(ctx context.Context, request graph.ListRequest) (graph.GetSetsResponse, error) {
+ ctx, span := observability.StartSpan("Memory Graph Provider", ctx, &map[string]string{
+ "method": "GetSets",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ seenSets := make(map[string]bool)
+ ret := graph.GetSetsResponse{
+ Sets: make(map[string]graph.GetSetResponse),
+ }
+ for _, node := range i.Data {
+ if request.Filter != "" && node.GetType() != request.Filter {
+ continue
+ }
+ if node.GetParent() == "" && !seenSets[node.GetId()] {
+ seenSets[node.GetId()] = true
+ var set graph.GetSetResponse
+ set, err = i.GetSet(ctx, graph.GetRequest{
+ Name: node.GetId(),
+ })
+ if err != nil {
+ return ret, err
+ }
+ ret.Sets[node.GetId()] = set
+ }
+ }
+ return ret, nil
+
+}
+func (i *MemoryGraphProvider) GetTrees(ctx context.Context, request graph.ListRequest) (graph.GetSetsResponse, error) {
+ ctx, span := observability.StartSpan("Memory Graph Provider", ctx, &map[string]string{
+ "method": "GetTrees",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ seenSets := make(map[string]bool)
+ ret := graph.GetSetsResponse{
+ Sets: make(map[string]graph.GetSetResponse),
+ }
+ for _, node := range i.Data {
+ if request.Filter != "" && node.GetType() != request.Filter {
+ continue
+ }
+ if node.GetParent() == "" && !seenSets[node.GetId()] {
+ seenSets[node.GetId()] = true
+ var set graph.GetSetResponse
+ set, err = i.GetTree(ctx, graph.GetRequest{
+ Name: node.GetId(),
+ })
+ if err != nil {
+ return ret, err
+ }
+ ret.Sets[node.GetId()] = set
+ }
+ }
+ return ret, nil
+}
+func (i *MemoryGraphProvider) GetChains(ctx context.Context, request graph.ListRequest) (graph.GetSetsResponse, error) {
+ ctx, span := observability.StartSpan("Memory Graph Provider", ctx, &map[string]string{
+ "method": "GetChains",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ seenSets := make(map[string]bool)
+ ret := graph.GetSetsResponse{
+ Sets: make(map[string]graph.GetSetResponse),
+ }
+ for _, node := range i.Data {
+ if request.Filter != "" && node.GetType() != request.Filter {
+ continue
+ }
+ if node.GetParent() == "" && !seenSets[node.GetId()] {
+ seenSets[node.GetId()] = true
+ var set graph.GetSetResponse
+ set, err = i.GetChain(ctx, graph.GetRequest{
+ Name: node.GetId(),
+ })
+ if err != nil {
+ return ret, err
+ }
+ ret.Sets[node.GetId()] = set
+ }
+ }
+ return ret, nil
+}
+func (i *MemoryGraphProvider) GetGraphs(ctx context.Context, request graph.ListRequest) (graph.GetGraphsResponse, error) {
+ return graph.GetGraphsResponse{}, v1alpha2.NewCOAError(nil, "not implemented", v1alpha2.NotImplemented)
+}
+
+func (i *MemoryGraphProvider) IsPure() bool {
+ return false
+}
+func (i *MemoryGraphProvider) SetData(data []v1alpha2.INode) error {
+ i.Data = data
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package counter
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "sync"
+
+ "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"
+)
+
+var msLock sync.Mutex
+
+type CounterStageProviderConfig struct {
+ ID string `json:"id"`
+}
+type CounterStageProvider struct {
+ Config CounterStageProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func (m *CounterStageProvider) Init(config providers.IProviderConfig) error {
+ msLock.Lock()
+ defer msLock.Unlock()
+
+ mockConfig, err := toMockStageProviderConfig(config)
+ if err != nil {
+ return err
+ }
+ m.Config = mockConfig
+ return nil
+}
+func (s *CounterStageProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+func toMockStageProviderConfig(config providers.IProviderConfig) (CounterStageProviderConfig, error) {
+ ret := CounterStageProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *CounterStageProvider) InitWithMap(properties map[string]string) error {
+ config, err := MockStageProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func MockStageProviderConfigFromMap(properties map[string]string) (CounterStageProviderConfig, error) {
+ ret := CounterStageProviderConfig{}
+ ret.ID = properties["id"]
+ return ret, nil
+}
+func (i *CounterStageProvider) Process(ctx context.Context, mgrContext contexts.ManagerContext, inputs map[string]interface{}) (map[string]interface{}, bool, error) {
+ _, span := observability.StartSpan("[Stage] Counter provider", ctx, &map[string]string{
+ "method": "Process",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ outputs := make(map[string]interface{})
+ selfState := make(map[string]interface{})
+ if state, ok := inputs["__state"]; ok {
+ selfState = state.(map[string]interface{})
+ }
+
+ for k, v := range inputs {
+ if k != "__state" {
+ if !strings.HasSuffix(k, ".init") {
+ var iv int64
+ if iv, err = getNumber(v); err == nil {
+ if s, ok := selfState[k]; ok {
+ var sv int64
+ if sv, err = getNumber(s); err == nil {
+ selfState[k] = sv + iv
+ outputs[k] = sv + iv
+ }
+ } else {
+ if vs, ok := inputs[k+".init"]; ok {
+ var ivs int64
+ if ivs, err = getNumber(vs); err == nil {
+ selfState[k] = ivs + iv
+ outputs[k] = ivs + iv
+ }
+ } else {
+ selfState[k] = iv
+ outputs[k] = iv
+ }
+ }
+ }
+ }
+ }
+ }
+
+ outputs["__state"] = selfState
+ return outputs, false, nil
+}
+
+func getNumber(val interface{}) (int64, error) {
+ if v, ok := val.(int64); ok {
+ return v, nil
+ }
+ if v, ok := val.(int); ok {
+ return int64(v), nil
+ }
+ if v, ok := val.(float64); ok {
+ return int64(v), nil
+ }
+ if v, ok := val.(float32); ok {
+ return int64(v), nil
+ }
+ if v, ok := val.(string); ok {
+ if v, err := strconv.ParseInt(v, 10, 64); err == nil {
+ return v, nil
+ }
+ }
+ return 0, fmt.Errorf("cannot convert %v to number", val)
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package create
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "sync"
+ "time"
+
+ "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"
+ "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"
+)
+
+var msLock sync.Mutex
+
+type CreateStageProviderConfig struct {
+ BaseUrl string `json:"baseUrl"`
+ User string `json:"user"`
+ Password string `json:"password"`
+ WaitCount int `json:"wait.count,omitempty"`
+ WaitInterval int `json:"wait.interval,omitempty"`
+}
+
+type CreateStageProvider struct {
+ Config CreateStageProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func (s *CreateStageProvider) Init(config providers.IProviderConfig) error {
+ msLock.Lock()
+ defer msLock.Unlock()
+ mockConfig, err := toSymphonyStageProviderConfig(config)
+ if err != nil {
+ return err
+ }
+ s.Config = mockConfig
+ return nil
+}
+func (s *CreateStageProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+func toSymphonyStageProviderConfig(config providers.IProviderConfig) (CreateStageProviderConfig, error) {
+ ret := CreateStageProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *CreateStageProvider) InitWithMap(properties map[string]string) error {
+ config, err := SymphonyStageProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func SymphonyStageProviderConfigFromMap(properties map[string]string) (CreateStageProviderConfig, error) {
+ ret := CreateStageProviderConfig{}
+ baseUrl, err := utils.GetString(properties, "baseUrl")
+ if err != nil {
+ return ret, err
+ }
+ ret.BaseUrl = baseUrl
+ if ret.BaseUrl == "" {
+ return ret, v1alpha2.NewCOAError(nil, "baseUrl is required", v1alpha2.BadConfig)
+ }
+ user, err := utils.GetString(properties, "user")
+ if err != nil {
+ return ret, err
+ }
+ ret.User = user
+ if ret.User == "" {
+ return ret, v1alpha2.NewCOAError(nil, "user is required", v1alpha2.BadConfig)
+ }
+ password, err := utils.GetString(properties, "password")
+ if err != nil {
+ return ret, err
+ }
+ waitStr, err := utils.GetString(properties, "wait.count")
+ if err != nil {
+ return ret, err
+ }
+ waitCount, err := strconv.Atoi(waitStr)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "wait.count must be an integer", v1alpha2.BadConfig)
+ }
+ ret.WaitCount = waitCount
+ waitStr, err = utils.GetString(properties, "wait.interval")
+ if err != nil {
+ return ret, err
+ }
+ waitInterval, err := strconv.Atoi(waitStr)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "wait.interval must be an integer", v1alpha2.BadConfig)
+ }
+ ret.WaitInterval = waitInterval
+ ret.Password = password
+ if waitCount <= 0 {
+ waitCount = 1
+ }
+ return ret, nil
+}
+func (i *CreateStageProvider) Process(ctx context.Context, mgrContext contexts.ManagerContext, inputs map[string]interface{}) (map[string]interface{}, bool, error) {
+ ctx, span := observability.StartSpan("[Stage] Create provider", ctx, &map[string]string{
+ "method": "Process",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ outputs := make(map[string]interface{})
+
+ objectType := stage.ReadInputString(inputs, "objectType")
+ objectName := stage.ReadInputString(inputs, "objectName")
+ action := stage.ReadInputString(inputs, "action")
+ object := inputs["object"]
+ var oData []byte
+ if object != nil {
+ oData, _ = json.Marshal(object)
+ }
+ lastSummaryMessage := ""
+ switch objectType {
+ case "instance":
+ objectScope := stage.ReadInputString(inputs, "objectScope")
+ if objectScope == "" {
+ objectScope = "default"
+ }
+
+ if action == "remove" {
+ err = utils.DeleteInstance(ctx, i.Config.BaseUrl, objectName, i.Config.User, i.Config.Password, objectScope)
+ if err != nil {
+ return nil, false, err
+ }
+ } else {
+ err = utils.CreateInstance(ctx, i.Config.BaseUrl, objectName, i.Config.User, i.Config.Password, oData, objectScope)
+ if err != nil {
+ return nil, false, err
+ }
+ for ic := 0; ic < i.Config.WaitCount; ic++ {
+ var summary model.SummaryResult
+ summary, err = utils.GetSummary(ctx, i.Config.BaseUrl, i.Config.User, i.Config.Password, objectName, objectScope)
+ lastSummaryMessage = summary.Summary.SummaryMessage
+ if err != nil {
+ return nil, false, err
+ }
+ if summary.Summary.SuccessCount == summary.Summary.TargetCount {
+ break
+ }
+ time.Sleep(time.Duration(i.Config.WaitInterval) * time.Second)
+ }
+ err = v1alpha2.NewCOAError(nil, fmt.Sprintf("Instance creation failed: %s", lastSummaryMessage), v1alpha2.InternalError)
+ return nil, false, err
+ }
+ }
+ outputs["objectType"] = objectType
+ outputs["objectName"] = objectName
+
+ return outputs, false, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package http
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "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/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"
+ coa_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var msLock sync.Mutex
+var sLog = logger.NewLogger("coa.runtime")
+
+type HttpStageProviderConfig struct {
+ Url string `json:"url"`
+ Method string `json:"method"`
+ SuccessCodes []int `json:"successCodes,omitempty"`
+ WaitUrl string `json:"wait.url,omitempty"`
+ WaitInterval int `json:"wait.interval,omitempty"`
+ WaitCount int `json:"wait.count,omitempty"`
+ WaitStartCodes []int `json:"wait.start,omitempty"`
+ WaitSuccessCodes []int `json:"wait.success,omitempty"`
+ WaitFailedCodes []int `json:"wait.fail,omitempty"`
+ WaitExpression string `json:"wait.expression,omitempty"`
+ WaitExpressionType string `json:"wait.expressionType,omitempty"`
+}
+type HttpStageProvider struct {
+ Config HttpStageProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func (m *HttpStageProvider) Init(config providers.IProviderConfig) error {
+ msLock.Lock()
+ defer msLock.Unlock()
+ sLog.Debug(" P (Http Stage): initialize")
+
+ mockConfig, err := toHttpStageProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): expected HttpStageProviderConfig: %+v", err)
+ return err
+ }
+ m.Config = mockConfig
+ return nil
+}
+func (s *HttpStageProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+func toHttpStageProviderConfig(config providers.IProviderConfig) (HttpStageProviderConfig, error) {
+ ret := HttpStageProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *HttpStageProvider) InitWithMap(properties map[string]string) error {
+ config, err := MockStageProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func MockStageProviderConfigFromMap(properties map[string]string) (HttpStageProviderConfig, error) {
+ ret := HttpStageProviderConfig{}
+ if v, ok := properties["url"]; ok {
+ ret.Url = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "missing required property url", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["method"]; ok {
+ ret.Method = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "missing required property method", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["successCodes"]; ok {
+ codes, err := readIntArray(v)
+ if err != nil {
+ return ret, err
+ }
+ ret.SuccessCodes = codes
+ }
+ if v, ok := properties["wait.success"]; ok {
+ codes, err := readIntArray(v)
+ if err != nil {
+ return ret, err
+ }
+ ret.SuccessCodes = codes
+ }
+ if v, ok := properties["wait.start"]; ok {
+ codes, err := readIntArray(v)
+ if err != nil {
+ return ret, err
+ }
+ ret.SuccessCodes = codes
+ }
+ if v, ok := properties["wait.fail"]; ok {
+ codes, err := readIntArray(v)
+ if err != nil {
+ return ret, err
+ }
+ ret.SuccessCodes = codes
+ }
+ if v, ok := properties["wait.url"]; ok {
+ ret.WaitUrl = v
+ }
+ if v, ok := properties["wait.interval"]; ok {
+ interval, err := strconv.Atoi(v)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, fmt.Sprintf("failed to parse wait interval %v", v), v1alpha2.BadConfig)
+ }
+ ret.WaitInterval = interval
+ }
+ if v, ok := properties["wait.count"]; ok {
+ count, err := strconv.Atoi(v)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, fmt.Sprintf("failed to parse wait count %v", v), v1alpha2.BadConfig)
+ }
+ ret.WaitCount = count
+ }
+ if v, ok := properties["wait.expression"]; ok {
+ ret.WaitExpression = v
+ }
+ if v, ok := properties["wait.expressionType"]; ok {
+ ret.WaitExpressionType = v
+ } else {
+ ret.WaitExpressionType = "symphony"
+ }
+ return ret, nil
+}
+func readIntArray(s string) ([]int, error) {
+ var codes []int
+ for _, code := range strings.Split(s, ",") {
+ code = strings.TrimSpace(code)
+ if code == "" {
+ continue
+ }
+ intCode, err := strconv.Atoi(code)
+ if err != nil {
+ return nil, v1alpha2.NewCOAError(err, fmt.Sprintf("failed to parse code %v", code), v1alpha2.BadConfig)
+ }
+ codes = append(codes, intCode)
+ }
+ return codes, nil
+}
+func (i *HttpStageProvider) Process(ctx context.Context, mgrContext contexts.ManagerContext, inputs map[string]interface{}) (map[string]interface{}, bool, error) {
+ _, span := observability.StartSpan("[Stage] Http provider", ctx, &map[string]string{
+ "method": "Process",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (Http Stage): start process request")
+
+ // Check all config fields for override in inputs
+ var configMap map[string]interface{}
+ configJson, _ := json.Marshal(i.Config)
+ json.Unmarshal(configJson, &configMap)
+ for key := range configMap {
+ val, found := inputs[key]
+ if found {
+ configMap[key] = val
+ }
+ }
+ configJson, err = json.Marshal(configMap)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to override config with input: %v", err)
+ return nil, false, err
+ }
+ err = json.Unmarshal(configJson, &i.Config)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to override config with input: %v", err)
+ return nil, false, err
+ }
+
+ sLog.Infof(" P (Http Stage): %v: %v", i.Config.Method, i.Config.Url)
+ webClient := &http.Client{}
+ req, err := http.NewRequest(fmt.Sprintf("%v", i.Config.Method), fmt.Sprintf("%v", i.Config.Url), nil)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to create request: %v", err)
+ return nil, false, err
+ }
+ for key, input := range inputs {
+ if strings.HasPrefix(key, "header.") {
+ req.Header.Add(key[7:], fmt.Sprintf("%v", input))
+ }
+ if key == "body" {
+ var jData []byte
+ jData, err = json.Marshal(input)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to encode json request body: %v", err)
+ return nil, false, err
+ }
+ req.Body = ioutil.NopCloser(bytes.NewBuffer(jData))
+ req.ContentLength = int64(len(jData))
+ }
+ }
+
+ resp, err := webClient.Do(req)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): request failed: %v", err)
+ return nil, false, err
+ }
+ defer resp.Body.Close()
+ outputs := make(map[string]interface{})
+
+ for key, values := range resp.Header {
+ outputs[fmt.Sprintf("header.%v", key)] = values
+ }
+
+ data, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to read request response: %v", err)
+ return nil, false, err
+ }
+ outputs["body"] = string(data) //TODO: probably not so good to assume string
+ outputs["status"] = resp.StatusCode
+
+ if i.Config.WaitUrl != "" {
+ okToWait := false
+ if len(i.Config.WaitStartCodes) > 0 {
+ for _, code := range i.Config.WaitStartCodes {
+ if code == resp.StatusCode {
+ okToWait = true
+ break
+ }
+ }
+ }
+ if !okToWait {
+ return nil, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("unexpected status code %v", resp.StatusCode), v1alpha2.BadConfig)
+ }
+ counter := 0
+ failed := false
+ succeeded := false
+ sLog.Debugf(" P (Http Stage): WaitCount: %d", i.Config.WaitCount)
+ for counter < i.Config.WaitCount || i.Config.WaitCount == 0 {
+ sLog.Infof(" P (Http Stage): start wait iteration %d", counter)
+ var waitReq *http.Request
+ waitReq, err = http.NewRequest("GET", i.Config.WaitUrl, nil)
+ for key, input := range inputs {
+ if strings.HasPrefix(key, "header.") {
+ waitReq.Header.Add(key[7:], fmt.Sprintf("%v", input))
+ }
+ }
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to create wait request: %v", err)
+ return nil, false, err
+ }
+ var waitResp *http.Response
+ waitResp, err = webClient.Do(waitReq)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): wait request failed: %v", err)
+ return nil, false, err
+ }
+ defer waitResp.Body.Close()
+ if len(i.Config.WaitFailedCodes) > 0 {
+ for _, code := range i.Config.WaitFailedCodes {
+ if code == waitResp.StatusCode {
+ failed = true
+ break
+ }
+ }
+ }
+ if len(i.Config.WaitSuccessCodes) > 0 {
+ for _, code := range i.Config.WaitSuccessCodes {
+ if code == waitResp.StatusCode {
+ succeeded = true
+ break
+ }
+ }
+ }
+ if succeeded {
+ var data []byte
+ data, err = ioutil.ReadAll(waitResp.Body)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to read wait request response: %v", err)
+ succeeded = false
+ } else {
+ if i.Config.WaitExpression != "" {
+ var obj interface{}
+ err = json.Unmarshal(data, &obj)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): wait response could not be decoded to json: %v", err)
+ succeeded = false
+ } else {
+ switch i.Config.WaitExpressionType {
+ case "jsonpath":
+ var result interface{}
+ result, err = utils.JsonPathQuery(obj, i.Config.WaitExpression)
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to evaluate JsonPath: %v", err)
+ }
+ succeeded = err == nil
+ outputs["waitResult"] = result
+ default:
+ parser := utils.NewParser(i.Config.WaitExpression)
+ var val interface{}
+ val, err = parser.Eval(coa_utils.EvaluationContext{
+ Value: obj,
+ })
+ if err != nil {
+ sLog.Errorf(" P (Http Stage): failed to evaluate Symphony expression: %v", err)
+ }
+ succeeded = (err == nil && val != "false") // a boolean Symphony expression may evaluate to "false" as a string, indicating the condition is not met
+ outputs["waitResult"] = val
+ }
+ }
+ }
+ if succeeded {
+ outputs["waitBody"] = string(data) //TODO: probably not so good to assume string
+ }
+ }
+ }
+ if !failed && !succeeded {
+ counter++
+ if i.Config.WaitInterval > 0 {
+ sLog.Debug(" P (Http Stage): sleep for wait interval")
+ time.Sleep(time.Duration(i.Config.WaitInterval) * time.Second)
+ }
+ } else {
+ break
+ }
+ }
+ if failed {
+ return nil, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("failed to wait for operation %v", resp.StatusCode), v1alpha2.BadConfig)
+ }
+
+ } else if len(i.Config.SuccessCodes) > 0 {
+ for _, code := range i.Config.SuccessCodes {
+ if code == resp.StatusCode {
+ return outputs, false, nil
+ }
+ }
+ sLog.Errorf(" P (Http Stage): failed to process request: %d", resp.StatusCode)
+ return nil, false, v1alpha2.NewCOAError(nil, fmt.Sprintf("unexpected status code %v", resp.StatusCode), v1alpha2.BadConfig)
+ }
+
+ sLog.Infof(" P (Http Stage): process request completed with: %d", resp.StatusCode)
+ return outputs, false, nil
+}
+func (*HttpStageProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{"header.*", "body"},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{},
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package patch
+
+import (
+ "context"
+ "encoding/json"
+ "sync"
+
+ "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"
+ "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"
+)
+
+var msLock sync.Mutex
+var sLog = logger.NewLogger("coa.runtime")
+
+type PatchStageProviderConfig struct {
+ BaseUrl string `json:"baseUrl"`
+ User string `json:"user"`
+ Password string `json:"password"`
+}
+
+type PatchStageProvider struct {
+ Config PatchStageProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func (s *PatchStageProvider) Init(config providers.IProviderConfig) error {
+ msLock.Lock()
+ defer msLock.Unlock()
+ mockConfig, err := toPatchStageProviderConfig(config)
+ if err != nil {
+ return err
+ }
+ s.Config = mockConfig
+ return nil
+}
+func (s *PatchStageProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+func toPatchStageProviderConfig(config providers.IProviderConfig) (PatchStageProviderConfig, error) {
+ ret := PatchStageProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *PatchStageProvider) InitWithMap(properties map[string]string) error {
+ config, err := SymphonyStageProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func SymphonyStageProviderConfigFromMap(properties map[string]string) (PatchStageProviderConfig, error) {
+ ret := PatchStageProviderConfig{}
+ baseUrl, err := utils.GetString(properties, "baseUrl")
+ if err != nil {
+ return ret, err
+ }
+ ret.BaseUrl = baseUrl
+ if ret.BaseUrl == "" {
+ return ret, v1alpha2.NewCOAError(nil, "baseUrl is required", v1alpha2.BadConfig)
+ }
+ user, err := utils.GetString(properties, "user")
+ if err != nil {
+ return ret, err
+ }
+ ret.User = user
+ if ret.User == "" {
+ return ret, v1alpha2.NewCOAError(nil, "user is required", v1alpha2.BadConfig)
+ }
+ password, err := utils.GetString(properties, "password")
+ if err != nil {
+ return ret, err
+ }
+ ret.Password = password
+ return ret, nil
+}
+func (m *PatchStageProvider) traceValue(v interface{}, ctx interface{}) (interface{}, error) {
+ switch val := v.(type) {
+ case string:
+ parser := utils.NewParser(val)
+ context := m.Context.VencorContext.EvaluationContext.Clone()
+ context.Value = ctx
+ v, err := parser.Eval(*context)
+ if err != nil {
+ return "", err
+ }
+ switch vt := v.(type) {
+ case string:
+ return vt, nil
+ default:
+ return m.traceValue(v, ctx)
+ }
+ case []interface{}:
+ ret := []interface{}{}
+ for _, v := range val {
+ tv, err := m.traceValue(v, ctx)
+ if err != nil {
+ return "", err
+ }
+ ret = append(ret, tv)
+ }
+ return ret, nil
+ case map[string]interface{}:
+ ret := map[string]interface{}{}
+ for k, v := range val {
+ tv, err := m.traceValue(v, ctx)
+ if err != nil {
+ return "", err
+ }
+ ret[k] = tv
+ }
+ return ret, nil
+ default:
+ return val, nil
+ }
+}
+
+func (i *PatchStageProvider) Process(ctx context.Context, mgrContext contexts.ManagerContext, inputs map[string]interface{}) (map[string]interface{}, bool, error) {
+ ctx, span := observability.StartSpan("[Stage] Patch Provider", ctx, &map[string]string{
+ "method": "Process",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (Patch Stage): start process request")
+
+ outputs := make(map[string]interface{})
+
+ objectType := stage.ReadInputString(inputs, "objectType")
+ objectName := stage.ReadInputString(inputs, "objectName")
+ patchSource := stage.ReadInputString(inputs, "patchSource")
+ var patchContent interface{}
+ if v, ok := inputs["patchContent"]; ok {
+ patchContent = v
+ }
+ componentName := stage.ReadInputString(inputs, "component")
+ propertyName := stage.ReadInputString(inputs, "property")
+ subKey := stage.ReadInputString(inputs, "subKey")
+ dedupKey := stage.ReadInputString(inputs, "dedupKey")
+ patchAction := stage.ReadInputString(inputs, "patchAction")
+ if patchAction == "" {
+ patchAction = "add"
+ }
+ udpated := false
+
+ var catalog model.CatalogState
+
+ switch patchSource {
+ case "", "catalog":
+ if v, ok := patchContent.(string); ok {
+ catalog, err = utils.GetCatalog(ctx, i.Config.BaseUrl, v, i.Config.User, i.Config.Password)
+
+ if err != nil {
+ sLog.Errorf(" P (Patch Stage): error getting catalog %s", v)
+ return nil, false, err
+ }
+ } else {
+ sLog.Errorf(" P (Patch Stage): error getting catalog %s", v)
+ err = v1alpha2.NewCOAError(nil, "patchContent is not valid", v1alpha2.BadConfig)
+ return nil, false, err
+ }
+ case "inline":
+ if componentName != "" {
+ if v, ok := patchContent.(map[string]interface{}); ok {
+ catalog = model.CatalogState{
+ Spec: &model.CatalogSpec{
+ Properties: v,
+ },
+ }
+ } else {
+ sLog.Errorf(" P (Patch Stage): error getting catalog %s", v)
+ err = v1alpha2.NewCOAError(nil, "patchContent is not valid", v1alpha2.BadConfig)
+ return nil, false, err
+ }
+ } else {
+ var componentSpec model.ComponentSpec
+ jData, _ := json.Marshal(patchContent)
+ if err = json.Unmarshal(jData, &componentSpec); err != nil {
+ sLog.Errorf(" P (Patch Stage): error unmarshalling componentSpec")
+ return nil, false, err
+ }
+ catalog = model.CatalogState{
+ Spec: &model.CatalogSpec{
+ Properties: map[string]interface{}{
+ "spec": componentSpec,
+ },
+ },
+ }
+ }
+ default:
+ sLog.Errorf(" P (Patch Stage): unsupported patchSource: %s", patchSource)
+ err = v1alpha2.NewCOAError(nil, "patchSource is not valid", v1alpha2.BadConfig)
+ return nil, false, err
+ }
+
+ for k, v := range catalog.Spec.Properties {
+ var tv interface{}
+ tv, err = i.traceValue(v, inputs["context"])
+ if err != nil {
+ sLog.Errorf(" P (Patch Stage): error tracing value %s", k)
+ return nil, false, err
+ }
+ catalog.Spec.Properties[k] = tv
+ }
+
+ switch objectType {
+ case "solution":
+ objectScope := stage.ReadInputString(inputs, "objectScope")
+ if objectScope == "" {
+ objectScope = "default"
+ }
+ var solution model.SolutionState
+ solution, err := utils.GetSolution(ctx, i.Config.BaseUrl, objectName, i.Config.User, i.Config.Password, objectScope)
+ if err != nil {
+ sLog.Errorf(" P (Patch Stage): error getting solution %s", objectName)
+ return nil, false, err
+ }
+
+ if componentName == "" {
+ componentSpec := catalog.Spec.Properties["spec"].(model.ComponentSpec)
+ for i, c := range solution.Spec.Components {
+ if c.Name == componentSpec.Name {
+ if patchAction == "remove" {
+ solution.Spec.Components = append(solution.Spec.Components[:i], solution.Spec.Components[i+1:]...)
+ } else {
+ solution.Spec.Components[i] = componentSpec
+ }
+ udpated = true
+ break
+ }
+ }
+ if !udpated && patchAction != "remove" {
+ solution.Spec.Components = append(solution.Spec.Components, componentSpec)
+ udpated = true
+ }
+ } else {
+ for i, c := range solution.Spec.Components {
+ if c.Name == componentName {
+ for k, p := range c.Properties {
+ if k == propertyName {
+ if subKey != "" {
+ if detailedTarget, ok := p.(map[string]interface{}); ok {
+ if v, ok := detailedTarget[subKey]; ok {
+ if targetMap, ok := v.([]interface{}); ok {
+ replaced := false
+ if dedupKey != "" {
+ for i, v := range targetMap {
+ if vmap, ok := v.(map[string]interface{}); ok {
+ if vmap[dedupKey] == catalog.Spec.Properties[dedupKey] {
+ if patchAction == "remove" {
+ targetMap = append(targetMap[:i], targetMap[i+1:]...)
+ } else {
+ targetMap[i] = catalog.Spec.Properties
+ }
+ replaced = true
+ break
+ }
+ }
+ }
+ }
+ if !replaced && patchAction != "remove" {
+ targetMap = append(targetMap, catalog.Spec.Properties)
+ }
+ detailedTarget[subKey] = targetMap
+ solution.Spec.Components[i].Properties[propertyName] = detailedTarget
+ udpated = true
+ } else {
+ sLog.Errorf(" P (Patch Stage): target properties is not valid")
+ err = v1alpha2.NewCOAError(nil, "target properties is not valid", v1alpha2.BadConfig)
+ return nil, false, err
+ }
+ } else {
+ sLog.Errorf(" P (Patch Stage): subKey is not valid")
+ err = v1alpha2.NewCOAError(nil, "subKey is not valid", v1alpha2.BadConfig)
+ return nil, false, err
+ }
+ } else {
+ sLog.Errorf(" P (Patch Stage): subKey is not valid")
+ err = v1alpha2.NewCOAError(nil, "subKey is not valid", v1alpha2.BadConfig)
+ return nil, false, err
+ }
+ } else {
+ if targetMap, ok := p.([]interface{}); ok {
+ replaced := false
+ if dedupKey != "" {
+ for i, v := range targetMap {
+ if vmap, ok := v.(map[string]interface{}); ok {
+ if vmap[dedupKey] == catalog.Spec.Properties[dedupKey] {
+ if patchAction == "remove" {
+ targetMap = append(targetMap[:i], targetMap[i+1:]...)
+ } else {
+ targetMap[i] = catalog.Spec.Properties
+ }
+ replaced = true
+ break
+ }
+ }
+ }
+ }
+ if !replaced && patchAction != "remove" {
+ targetMap = append(targetMap, catalog.Spec.Properties)
+ }
+ solution.Spec.Components[i].Properties[propertyName] = targetMap
+ udpated = true
+ } else {
+ sLog.Errorf(" P (Patch Stage): target properties is not valid")
+ err = v1alpha2.NewCOAError(nil, "target properties is not valid", v1alpha2.BadConfig)
+ return nil, false, err
+ }
+ }
+ break
+ }
+ }
+ break
+ }
+ }
+ }
+ if udpated {
+ jData, _ := json.Marshal(solution.Spec)
+ err := utils.UpsertSolution(ctx, i.Config.BaseUrl, objectName, i.Config.User, i.Config.Password, jData, objectScope)
+ if err != nil {
+ sLog.Errorf(" P (Patch Stage): error updating solution %s", objectName)
+ return nil, false, err
+ }
+ }
+
+ }
+ sLog.Info(" P (Patch Stage): end process request")
+ return outputs, false, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package script
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "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"
+ "github.com/google/uuid"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type ScriptStageProviderConfig struct {
+ Name string `json:"name"`
+ Script string `json:"script"`
+ ScriptFolder string `json:"scriptFolder,omitempty"`
+ StagingFolder string `json:"stagingFolder,omitempty"`
+ ScriptEngine string `json:"scriptEngine,omitempty"`
+}
+
+type ScriptStageProvider struct {
+ Config ScriptStageProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func ScriptProviderConfigFromMap(properties map[string]string) (ScriptStageProviderConfig, error) {
+ ret := ScriptStageProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["stagingFolder"]; ok {
+ ret.StagingFolder = v
+ }
+ if v, ok := properties["scriptFolder"]; ok {
+ ret.ScriptFolder = v
+ }
+ if v, ok := properties["script"]; ok {
+ ret.Script = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "invalid script provider config, exptected 'script'", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["scriptEngine"]; ok {
+ ret.ScriptEngine = v
+ } else {
+ ret.ScriptEngine = "bash"
+ }
+ if ret.ScriptEngine != "bash" && ret.ScriptEngine != "powershell" {
+ return ret, v1alpha2.NewCOAError(nil, "invalid script engine, exptected 'bash' or 'powershell'", v1alpha2.BadConfig)
+ }
+ return ret, nil
+}
+func (i *ScriptStageProvider) InitWithMap(properties map[string]string) error {
+ config, err := ScriptProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *ScriptStageProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *ScriptStageProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("[Stage] Script Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (Script Stage): Init()")
+
+ updateConfig, err := toScriptStageProviderConfig(config)
+ if err != nil {
+ err = errors.New("expected ScriptProviderConfig")
+ return err
+ }
+ i.Config = updateConfig
+
+ if strings.HasPrefix(i.Config.ScriptFolder, "http") {
+ err = downloadFile(i.Config.ScriptFolder, i.Config.Script, i.Config.StagingFolder)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+func downloadFile(scriptFolder string, script string, stagingFolder string) error {
+ sPath, err := url.JoinPath(scriptFolder, script)
+ if err != nil {
+ return err
+ }
+ tPath := filepath.Join(stagingFolder, script)
+
+ out, err := os.Create(tPath)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ resp, err := http.Get(sPath)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ _, err = io.Copy(out, resp.Body)
+ if err != nil {
+ return err
+ }
+ return os.Chmod(tPath, 0755)
+}
+func toScriptStageProviderConfig(config providers.IProviderConfig) (ScriptStageProviderConfig, error) {
+ ret := ScriptStageProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+func (i *ScriptStageProvider) Process(ctx context.Context, mgrContext contexts.ManagerContext, inputs map[string]interface{}) (map[string]interface{}, bool, error) {
+ _, span := observability.StartSpan("[Stage] Script Provider", ctx, &map[string]string{
+ "method": "Process",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (Script Stage): start process request")
+
+ id := uuid.New().String()
+ input := id + ".json"
+ output := id + "-output.json"
+
+ staging := filepath.Join(i.Config.StagingFolder, input)
+ file, _ := json.MarshalIndent(inputs, "", " ")
+ _ = ioutil.WriteFile(staging, file, 0644)
+
+ abs, _ := filepath.Abs(staging)
+
+ defer os.Remove(abs)
+
+ scriptAbs, _ := filepath.Abs(filepath.Join(i.Config.ScriptFolder, i.Config.Script))
+ if strings.HasPrefix(i.Config.ScriptFolder, "http") {
+ scriptAbs, _ = filepath.Abs(filepath.Join(i.Config.StagingFolder, i.Config.Script))
+ }
+
+ o, err := i.runCommand(scriptAbs, abs)
+ sLog.Debugf(" P (Script Stage): get script output: %s", o)
+
+ if err != nil {
+ sLog.Errorf(" P (Script Stage): failed to run get script: %+v", err)
+ return nil, false, err
+ }
+
+ outputStaging := filepath.Join(i.Config.StagingFolder, output)
+
+ data, err := ioutil.ReadFile(outputStaging)
+
+ if err != nil {
+ sLog.Errorf(" P (Script Stage): failed to parse get script output (expected map[string]interface{}): %+v", err)
+ return nil, false, err
+ }
+
+ abs_output, _ := filepath.Abs(outputStaging)
+
+ defer os.Remove(abs_output)
+
+ ret := make(map[string]interface{})
+ err = json.Unmarshal(data, &ret)
+ if err != nil {
+ sLog.Errorf(" P (Script Stage): failed to parse get script output (expected map[string]interface{}): %+v", err)
+ return nil, false, err
+ }
+
+ return ret, false, nil
+}
+
+func (i *ScriptStageProvider) runCommand(scriptAbs string, parameters ...string) ([]byte, error) {
+ // Sanitize input to prevent command injection
+ scriptAbs = strings.ReplaceAll(scriptAbs, "|", "")
+ scriptAbs = strings.ReplaceAll(scriptAbs, "&", "")
+ for idx, param := range parameters {
+ parameters[idx] = strings.ReplaceAll(param, "|", "")
+ parameters[idx] = strings.ReplaceAll(param, "&", "")
+ }
+
+ var err error
+ var out []byte
+ params := make([]string, 0)
+ if i.Config.ScriptEngine == "" || i.Config.ScriptEngine == "bash" {
+ params = append(params, parameters...)
+ out, err = exec.Command(scriptAbs, params...).Output()
+ } else {
+ params = append(params, scriptAbs)
+ params = append(params, parameters...)
+ out, err = exec.Command("powershell", params...).Output()
+ }
+ return out, err
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package k8s
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "strconv"
+
+ "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/apis/v1alpha2/providers/states"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+ k8s_errors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/util/homedir"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type K8sStateProviderConfig struct {
+ Name string `json:"name"`
+ ConfigType string `json:"configType,omitempty"`
+ ConfigData string `json:"configData,omitempty"`
+ Context string `json:"context,omitempty"`
+ InCluster bool `json:"inCluster"`
+}
+
+type K8sStateProvider struct {
+ Config K8sStateProviderConfig
+ Context *contexts.ManagerContext
+ DynamicClient dynamic.Interface
+}
+
+func K8sStateProviderConfigFromMap(properties map[string]string) (K8sStateProviderConfig, error) {
+ ret := K8sStateProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["configType"]; ok {
+ ret.ConfigType = v
+ }
+ if v, ok := properties["configData"]; ok {
+ ret.ConfigData = v
+ }
+ if v, ok := properties["context"]; ok {
+ ret.Context = v
+ }
+ if ret.ConfigType == "" {
+ ret.ConfigType = "path"
+ }
+ if v, ok := properties["inCluster"]; ok {
+ val := v
+ if val != "" {
+ bVal, err := strconv.ParseBool(val)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid bool value in the 'inCluster' setting of K8s state provider", v1alpha2.BadConfig)
+ }
+ ret.InCluster = bVal
+ }
+ }
+ return ret, nil
+}
+
+func (i *K8sStateProvider) InitWithMap(properties map[string]string) error {
+ config, err := K8sStateProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *K8sStateProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *K8sStateProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("K8s State Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Debug(" P (K8s State): initialize")
+
+ updateConfig, err := toK8sStateProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): expected KubectlTargetProviderConfig: %+v", err)
+ return err
+ }
+ i.Config = updateConfig
+ var kConfig *rest.Config
+ if i.Config.InCluster {
+ kConfig, err = rest.InClusterConfig()
+ } else {
+ switch i.Config.ConfigType {
+ case "path":
+ if i.Config.ConfigData == "" {
+ if home := homedir.HomeDir(); home != "" {
+ i.Config.ConfigData = filepath.Join(home, ".kube", "config")
+ } else {
+ err = v1alpha2.NewCOAError(nil, "can't locate home direction to read default kubernetes config file, to run in cluster, set inCluster config setting to true", v1alpha2.BadConfig)
+ sLog.Errorf(" P (K8s State): %+v", err)
+ return err
+ }
+ }
+ kConfig, err = clientcmd.BuildConfigFromFlags("", i.Config.ConfigData)
+ case "bytes":
+ if i.Config.ConfigData != "" {
+ kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData))
+ if err != nil {
+ sLog.Errorf(" P (K8s State): %+v", err)
+ return err
+ }
+ } else {
+ err = v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig)
+ sLog.Errorf(" P (K8s State): %+v", err)
+ return err
+ }
+ default:
+ err = v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and bytes", v1alpha2.BadConfig)
+ sLog.Errorf(" P (K8s State): %+v", err)
+ return err
+ }
+ }
+ if err != nil {
+ sLog.Errorf(" P (K8s State): %+v", err)
+ return err
+ }
+ i.DynamicClient, err = dynamic.NewForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): %+v", err)
+ return err
+ }
+
+ return nil
+}
+
+func toK8sStateProviderConfig(config providers.IProviderConfig) (K8sStateProviderConfig, error) {
+ ret := K8sStateProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ if ret.ConfigType == "" {
+ ret.ConfigType = "path"
+ }
+ return ret, err
+}
+
+func (s *K8sStateProvider) Upsert(ctx context.Context, entry states.UpsertRequest) (string, error) {
+ ctx, span := observability.StartSpan("K8s State Provider", ctx, &map[string]string{
+ "method": "Upsert",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (K8s State): upsert state")
+
+ scope := model.ReadProperty(entry.Metadata, "scope", nil)
+ group := model.ReadProperty(entry.Metadata, "group", nil)
+ version := model.ReadProperty(entry.Metadata, "version", nil)
+ resource := model.ReadProperty(entry.Metadata, "resource", nil)
+
+ if scope == "" {
+ scope = "default"
+ }
+
+ resourceId := schema.GroupVersionResource{
+ Group: group,
+ Version: version,
+ Resource: resource,
+ }
+
+ j, _ := json.Marshal(entry.Value.Body)
+
+ item, err := s.DynamicClient.Resource(resourceId).Namespace(scope).Get(ctx, entry.Value.ID, metav1.GetOptions{})
+ if err != nil {
+ // TODO: check if not-found error
+ template := model.ReadProperty(entry.Metadata, "template", &model.ValueInjections{
+ TargetId: entry.Value.ID,
+ SolutionId: entry.Value.ID, //TODO: This is not very nice. Maybe change ValueInjection to include a generic ID?
+ InstanceId: entry.Value.ID,
+ ActivationId: entry.Value.ID,
+ CampaignId: entry.Value.ID,
+ CatalogId: entry.Value.ID,
+ DeviceId: entry.Value.ID,
+ ModelId: entry.Value.ID,
+ SkillId: entry.Value.ID,
+ SiteId: entry.Value.ID,
+ })
+ var unc *unstructured.Unstructured
+ err = json.Unmarshal([]byte(template), &unc)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to deserialize template: %v", err)
+ return "", err
+ }
+ var dict map[string]interface{}
+ err = json.Unmarshal(j, &dict)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to get object: %v", err)
+ return "", err
+ }
+ unc.Object["spec"] = dict["spec"]
+ _, err = s.DynamicClient.Resource(resourceId).Namespace(scope).Create(ctx, unc, metav1.CreateOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to create object: %v", err)
+ return "", err
+ }
+ //Note: state is ignored for new object
+ } else {
+ j, _ := json.Marshal(entry.Value.Body)
+ var dict map[string]interface{}
+ err = json.Unmarshal(j, &dict)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to unmarshal object: %v", err)
+ return "", err
+ }
+ if v, ok := dict["spec"]; ok {
+ item.Object["spec"] = v
+
+ _, err = s.DynamicClient.Resource(resourceId).Namespace(scope).Update(ctx, item, metav1.UpdateOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to update object: %v", err)
+ return "", err
+ }
+ }
+ if v, ok := dict["status"]; ok {
+ status := &unstructured.Unstructured{
+ Object: map[string]interface{}{
+ "apiVersion": group + "/" + version,
+ "kind": "Status",
+ "metadata": map[string]interface{}{
+ "name": entry.Value.ID,
+ },
+ "status": v.(map[string]interface{}),
+ },
+ }
+ status.SetResourceVersion(item.GetResourceVersion())
+ _, err = s.DynamicClient.Resource(resourceId).Namespace(scope).UpdateStatus(ctx, status, v1.UpdateOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to update object status: %v", err)
+ return "", err
+ }
+ }
+ }
+ return entry.Value.ID, nil
+}
+
+func (s *K8sStateProvider) ListAllNamespaces(ctx context.Context, version string) ([]string, error) {
+ namespaceResource := schema.GroupVersionResource{Group: "", Version: version, Resource: "namespaces"}
+ namespaces, err := s.DynamicClient.Resource(namespaceResource).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return nil, err
+ }
+ ret := []string{}
+ for _, namespace := range namespaces.Items {
+ ret = append(ret, namespace.GetName())
+ }
+ return ret, err
+}
+
+func (s *K8sStateProvider) List(ctx context.Context, request states.ListRequest) ([]states.StateEntry, string, error) {
+ var entities []states.StateEntry
+
+ ctx, span := observability.StartSpan("K8s State Provider", ctx, &map[string]string{
+ "method": "List",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (K8s State): list state")
+
+ scope := model.ReadProperty(request.Metadata, "scope", nil)
+ group := model.ReadProperty(request.Metadata, "group", nil)
+ version := model.ReadProperty(request.Metadata, "version", nil)
+ resource := model.ReadProperty(request.Metadata, "resource", nil)
+
+ var namespaces []string
+ if scope == "" {
+ ret, err := s.ListAllNamespaces(ctx, version)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to list namespaces: %v", err)
+ return nil, "", err
+ }
+ namespaces = ret
+ } else {
+ namespaces = []string{scope}
+ }
+ for _, namespace := range namespaces {
+ resourceId := schema.GroupVersionResource{
+ Group: group,
+ Version: version,
+ Resource: resource,
+ }
+ items, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to list objects in namespace %s: %v ", namespace, err)
+ return nil, "", err
+ }
+ for _, v := range items.Items {
+ generation := v.GetGeneration()
+ entry := states.StateEntry{
+ ETag: strconv.FormatInt(generation, 10),
+ ID: v.GetName(),
+ Body: map[string]interface{}{
+ "spec": v.Object["spec"],
+ "status": v.Object["status"],
+ "scope": namespace,
+ },
+ }
+ entities = append(entities, entry)
+ }
+ }
+ return entities, "", nil
+}
+
+func (s *K8sStateProvider) Delete(ctx context.Context, request states.DeleteRequest) error {
+ ctx, span := observability.StartSpan("K8s State Provider", ctx, &map[string]string{
+ "method": "Delete",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (K8s State): delete state")
+
+ scope := model.ReadProperty(request.Metadata, "scope", nil)
+ group := model.ReadProperty(request.Metadata, "group", nil)
+ version := model.ReadProperty(request.Metadata, "version", nil)
+ resource := model.ReadProperty(request.Metadata, "resource", nil)
+
+ resourceId := schema.GroupVersionResource{
+ Group: group,
+ Version: version,
+ Resource: resource,
+ }
+ if scope == "" {
+ scope = "default"
+ }
+
+ err = s.DynamicClient.Resource(resourceId).Namespace(scope).Delete(ctx, request.ID, metav1.DeleteOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to delete objects: %v", err)
+ return err
+ }
+ return nil
+}
+
+func (s *K8sStateProvider) Get(ctx context.Context, request states.GetRequest) (states.StateEntry, error) {
+ ctx, span := observability.StartSpan("K8s State Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (K8s State): get state")
+
+ scope := model.ReadProperty(request.Metadata, "scope", nil)
+ group := model.ReadProperty(request.Metadata, "group", nil)
+ version := model.ReadProperty(request.Metadata, "version", nil)
+ resource := model.ReadProperty(request.Metadata, "resource", nil)
+
+ if scope == "" {
+ scope = "default"
+ }
+
+ resourceId := schema.GroupVersionResource{
+ Group: group,
+ Version: version,
+ Resource: resource,
+ }
+
+ item, err := s.DynamicClient.Resource(resourceId).Namespace(scope).Get(ctx, request.ID, metav1.GetOptions{})
+ if err != nil {
+ coaError := v1alpha2.NewCOAError(err, "failed to get object", v1alpha2.InternalError)
+ //check if not found
+ if k8s_errors.IsNotFound(err) {
+ coaError.State = v1alpha2.NotFound
+ }
+ sLog.Errorf(" P (K8s State %v", coaError.Error())
+ return states.StateEntry{}, coaError
+ }
+ generation := item.GetGeneration()
+ ret := states.StateEntry{
+ ID: request.ID,
+ ETag: strconv.FormatInt(generation, 10),
+ Body: map[string]interface{}{
+ "spec": item.Object["spec"],
+ "status": item.Object["status"],
+ "scope": scope,
+ },
+ }
+ return ret, nil
+}
+
+// Implmeement the IConfigProvider interface
+func (s *K8sStateProvider) Read(object string, field string) (string, error) {
+ obj, err := s.Get(context.TODO(), states.GetRequest{
+ ID: object,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "catalogs",
+ },
+ })
+ if err != nil {
+ return "", err
+ }
+ if v, ok := obj.Body.(map[string]interface{})["spec"]; ok {
+ spec := v.(map[string]interface{})
+ if v, ok := spec["properties"]; ok {
+ properties := v.(map[string]interface{})
+ if v, ok := properties[field]; ok {
+ return v.(string), nil
+ } else {
+ return "", v1alpha2.NewCOAError(nil, fmt.Sprintf("field '%s' is not found in configuration catalog '%s'", field, object), v1alpha2.NotFound)
+ }
+ } else {
+ return "", v1alpha2.NewCOAError(nil, "properties not found", v1alpha2.NotFound)
+ }
+ }
+ return "", v1alpha2.NewCOAError(nil, "spec not found", v1alpha2.NotFound)
+}
+
+func (s *K8sStateProvider) ReadObject(object string) (map[string]string, error) {
+ obj, err := s.Get(context.TODO(), states.GetRequest{
+ ID: object,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "catalogs",
+ },
+ })
+ if err != nil {
+ return nil, err
+ }
+ if v, ok := obj.Body.(map[string]interface{})["spec"]; ok {
+ spec := v.(map[string]interface{})
+ if v, ok := spec["properties"]; ok {
+ properties := v.(map[string]interface{})
+ ret := map[string]string{}
+ for k, v := range properties {
+ ret[k] = v.(string)
+ }
+ return ret, nil
+ } else {
+ return nil, v1alpha2.NewCOAError(nil, "properties not found", v1alpha2.NotFound)
+ }
+ }
+ return nil, v1alpha2.NewCOAError(nil, "spec not found", v1alpha2.NotFound)
+}
+
+func (s *K8sStateProvider) Set(object string, field string, value string, scope string) error {
+ obj, err := s.Get(context.TODO(), states.GetRequest{
+ ID: object,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "catalogs",
+ "scope": scope,
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if v, ok := obj.Body.(map[string]interface{})["spec"]; ok {
+ spec := v.(map[string]interface{})
+ if v, ok := spec["properties"]; ok {
+ properties := v.(map[string]interface{})
+ properties[field] = value
+ _, err := s.Upsert(context.TODO(), states.UpsertRequest{
+ Value: obj,
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Catalog", "metadata": {"name": "${{$catalog()}}"}}`, model.FederationGroup),
+ "scope": scope,
+ "group": model.FederationGroup,
+ "version": "v1",
+ "resource": "catalogs",
+ },
+ })
+ return err
+ } else {
+ return v1alpha2.NewCOAError(nil, "properties not found", v1alpha2.NotFound)
+ }
+ }
+ return v1alpha2.NewCOAError(nil, "spec not found", v1alpha2.NotFound)
+}
+func (s *K8sStateProvider) SetObject(object string, values map[string]string, scope string) error {
+ obj, err := s.Get(context.TODO(), states.GetRequest{
+ ID: object,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "catalogs",
+ "scope": scope,
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if v, ok := obj.Body.(map[string]interface{})["spec"]; ok {
+ spec := v.(map[string]interface{})
+ if v, ok := spec["properties"]; ok {
+ properties := v.(map[string]interface{})
+ for k, v := range values {
+ properties[k] = v
+ }
+ _, err := s.Upsert(context.TODO(), states.UpsertRequest{
+ Value: obj,
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Catalog", "metadata": {"name": "${{$catalog()}}"}}`, model.FederationGroup),
+ "scope": scope,
+ "group": model.FederationGroup,
+ "version": "v1",
+ "resource": "catalogs",
+ },
+ })
+ return err
+ } else {
+ return v1alpha2.NewCOAError(nil, "properties not found", v1alpha2.NotFound)
+ }
+ }
+ return v1alpha2.NewCOAError(nil, "spec not found", v1alpha2.NotFound)
+}
+func (s *K8sStateProvider) Remove(object string, field string, scope string) error {
+ obj, err := s.Get(context.TODO(), states.GetRequest{
+ ID: object,
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FederationGroup,
+ "resource": "catalogs",
+ "scope": scope,
+ },
+ })
+ if err != nil {
+ return err
+ }
+ if v, ok := obj.Body.(map[string]interface{})["spec"]; ok {
+ spec := v.(map[string]interface{})
+ if v, ok := spec["properties"]; ok {
+ properties := v.(map[string]interface{})
+ delete(properties, field)
+ _, err := s.Upsert(context.TODO(), states.UpsertRequest{
+ Value: obj,
+ Metadata: map[string]string{
+ "template": fmt.Sprintf(`{"apiVersion":"%s/v1", "kind": "Catalog", "metadata": {"name": "${{$catalog()}}"}}`, model.FederationGroup),
+ "scope": scope,
+ "group": model.FederationGroup,
+ "version": "v1",
+ "resource": "catalogs",
+ },
+ })
+ return err
+ } else {
+ return v1alpha2.NewCOAError(nil, "properties not found", v1alpha2.NotFound)
+ }
+ }
+ return v1alpha2.NewCOAError(nil, "spec not found", v1alpha2.NotFound)
+}
+func (s *K8sStateProvider) RemoveObject(object string, scope string) error {
+ return s.Delete(context.TODO(), states.DeleteRequest{
+ ID: object,
+ Metadata: map[string]string{
+ "scope": scope,
+ "group": model.FederationGroup,
+ "version": "v1",
+ "resource": "catalogs",
+ },
+ })
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package adb
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os/exec"
+ "regexp"
+ "strings"
+
+ "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"
+)
+
+var aLog = logger.NewLogger("coa.runtime")
+
+type AdbProviderConfig struct {
+ Name string `json:"name"`
+}
+
+type AdbProvider struct {
+ Config AdbProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func AdbProviderConfigFromMap(properties map[string]string) (AdbProviderConfig, error) {
+ ret := AdbProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ return ret, nil
+}
+
+func (i *AdbProvider) InitWithMap(properties map[string]string) error {
+ config, err := AdbProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func (s *AdbProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *AdbProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("Android ADB Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ aLog.Info(" P (Android ADB): Init()")
+
+ updateConfig, err := toAdbProviderConfig(config)
+ if err != nil {
+ return errors.New("expected AdbProviderConfig")
+ }
+ i.Config = updateConfig
+ return nil
+}
+
+func toAdbProviderConfig(config providers.IProviderConfig) (AdbProviderConfig, error) {
+ ret := AdbProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+func (i *AdbProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ _, span := observability.StartSpan("Android ADB Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ aLog.Infof(" P (Android ADB): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ ret := make([]model.ComponentSpec, 0)
+
+ re := regexp.MustCompile(`^package:(\w+\.)+\w+$`)
+
+ for _, component := range references {
+ if p, ok := component.Component.Properties[model.AppPackage]; ok {
+ params := make([]string, 0)
+ params = append(params, "shell")
+ params = append(params, "pm")
+ params = append(params, "list")
+ params = append(params, "packages")
+ params = append(params, fmt.Sprintf("%v", p))
+ var out []byte
+ out, err = exec.Command("adb", params...).Output()
+
+ if err != nil {
+ return nil, err
+ }
+ str := string(out)
+ lines := strings.Split(str, "\r\n")
+ for _, line := range lines {
+ if re.Match([]byte(line)) {
+ ret = append(ret, model.ComponentSpec{
+ Name: line[8:],
+ Type: model.AppPackage,
+ })
+ }
+ }
+ }
+ }
+ return ret, nil
+}
+
+func (i *AdbProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("Android ADB Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ aLog.Infof(" P (Android ADB Provider): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ components := step.GetComponents()
+
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+ ret := step.PrepareResultMap()
+ components = step.GetUpdatedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Name != "" {
+ if p, ok := component.Properties[model.AppImage]; ok && p != "" {
+ if !isDryRun {
+ params := make([]string, 0)
+ params = append(params, "install")
+ params = append(params, p.(string))
+ cmd := exec.Command("adb", params...)
+ err = cmd.Run()
+ if err != nil {
+ ret[component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ return ret, err
+ }
+ }
+ }
+ }
+ }
+ }
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Name != "" {
+ if p, ok := component.Properties[model.AppPackage]; ok && p != "" {
+ params := make([]string, 0)
+ params = append(params, "uninstall")
+ params = append(params, p.(string))
+
+ cmd := exec.Command("adb", params...)
+ err = cmd.Run()
+ if err != nil {
+ ret[component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.DeleteFailed,
+ Message: err.Error(),
+ }
+ return ret, err
+ }
+ }
+ }
+ }
+ }
+ err = nil
+ return ret, nil
+}
+
+func (*AdbProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{model.AppPackage, model.AppImage},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package adu
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "time"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+ azureutils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/cloudutils/azure"
+ "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"
+ "github.com/google/uuid"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type ADUTargetProviderConfig struct {
+ Name string `json:"name"`
+ TenantId string `json:"tenantId"`
+ ClientId string `json:"clientId"`
+ ClientSecret string `json:"clientSecret"`
+ ADUAccountEndpoint string `json:"aduAccountEndpoint"`
+ ADUAccountInstance string `json:"aduAccountInstance"`
+ ADUGroup string `json:"aduGroup"`
+}
+
+type ADUTargetProvider struct {
+ Config ADUTargetProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func ADUTargetProviderConfigFromMap(properties map[string]string) (ADUTargetProviderConfig, error) {
+ ret := ADUTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["tenantId"]; ok {
+ ret.TenantId = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "ADU update provider tenant id is not set", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["clientId"]; ok {
+ ret.ClientId = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "ADU update provider client id is not set", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["clientSecret"]; ok {
+ ret.ClientSecret = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "ADU update provider client secret is not set", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["aduAccountEndpoint"]; ok {
+ ret.ADUAccountEndpoint = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "ADU update account endpoint is not set", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["aduAccountInstance"]; ok {
+ ret.ADUAccountInstance = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "ADU update account instance is not set", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["aduGroup"]; ok {
+ ret.ADUGroup = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "ADU update group is not set", v1alpha2.BadConfig)
+ }
+ return ret, nil
+}
+
+func (i *ADUTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := ADUTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *ADUTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *ADUTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("ADU Target Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info("~~~ ADU Target Provider ~~~ : Init()")
+
+ updateConfig, err := toADUTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf("~~~ ADU Target Provider ~~~ : expected ADUTargetProviderConfig: %+v", err)
+ return err
+ }
+ i.Config = updateConfig
+ return nil
+}
+
+func toADUTargetProviderConfig(config providers.IProviderConfig) (ADUTargetProviderConfig, error) {
+ ret := ADUTargetProviderConfig{}
+ if config == nil {
+ return ret, errors.New("ADUTargetProviderConfig is null")
+ }
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+
+func (i *ADUTargetProvider) Get(ctx context.Context, dep model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ _, span := observability.StartSpan("ADU Target Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info("~~~ ADU Update Provider ~~~ : getting components")
+ deployment, err := i.getDeployment()
+ if err != nil {
+ sLog.Errorf("~~~ ADU Target Provider ~~~ : %+v", err)
+ return nil, err
+ }
+
+ ret := []model.ComponentSpec{}
+
+ if deployment.DeploymentId != "" {
+ ret = append(ret, model.ComponentSpec{
+ Name: deployment.UpdateId.Name,
+ Properties: map[string]interface{}{
+ "update.name": deployment.UpdateId.Name,
+ "update.provider": deployment.UpdateId.Provider,
+ "update.version": deployment.UpdateId.Version,
+ },
+ })
+ }
+
+ return ret, nil
+}
+
+func getDeploymentFromComponent(c model.ComponentSpec) (azureutils.ADUDeployment, error) {
+ provider := ""
+ version := ""
+ name := ""
+ ok := false
+ deployment := azureutils.ADUDeployment{}
+ if provider, ok = c.Properties["update.provider"].(string); !ok {
+ return deployment, errors.New("component doesn't contain a update.provider property")
+ }
+ if version, ok = c.Properties["update.version"].(string); !ok {
+ return deployment, errors.New("component doesn't contain a update.version property")
+ }
+ if name, ok = c.Properties["update.name"].(string); !ok {
+ return deployment, errors.New("component doesn't contain a update.name property")
+ }
+ deployment.DeploymentId = uuid.New().String()
+ deployment.StartDateTime = time.Now().UTC().Format("2006-01-02T15:04:05-0700")
+ deployment.UpdateId = azureutils.UpdateId{
+ Name: name,
+ Provider: provider,
+ Version: version,
+ }
+ return deployment, nil
+}
+
+func (i *ADUTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("ADU Target Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (ADU Update): applying components")
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+
+ for _, c := range step.Components {
+ var deployment azureutils.ADUDeployment
+ deployment, err = getDeploymentFromComponent(c.Component)
+ if err != nil {
+ ret[c.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.ValidateFailed,
+ Message: err.Error(),
+ }
+ return ret, err
+ }
+ if c.Action == "update" {
+ deployment.GroupId = i.Config.ADUGroup
+ err = i.applyDeployment(deployment)
+ if err != nil {
+ ret[c.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P (ADU Update): %+v", err)
+ return ret, err
+ }
+ } else {
+ err = i.deleteDeploymeent(deployment)
+ if err != nil {
+ ret[c.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.DeleteFailed,
+ Message: err.Error(),
+ }
+ err = nil
+ return ret, nil //TODO: are we ignoring errors on purpose here?
+ }
+ }
+
+ }
+ return ret, nil
+}
+
+func (i *ADUTargetProvider) getDeployment() (azureutils.ADUDeployment, error) {
+ ret := azureutils.ADUDeployment{}
+ token, err := azureutils.GetAzureToken(i.Config.TenantId, i.Config.ClientId, i.Config.ClientSecret, "https://api.adu.microsoft.com/.default")
+ if err != nil {
+ return ret, err
+ }
+ group, err := azureutils.GetADUGroup(token, i.Config.ADUAccountEndpoint, i.Config.ADUAccountInstance, i.Config.ADUGroup)
+ if err != nil {
+ return ret, err
+ }
+ if group.DeploymentId == "" {
+ return ret, nil
+ }
+ deployment, err := azureutils.GetADUDeployment(token, i.Config.ADUAccountEndpoint, i.Config.ADUAccountInstance, i.Config.ADUGroup, group.DeploymentId)
+ if err != nil {
+ return ret, err
+ }
+ return deployment, nil
+}
+func (i *ADUTargetProvider) deleteDeploymeent(deployment azureutils.ADUDeployment) error {
+ token, err := azureutils.GetAzureToken(i.Config.TenantId, i.Config.ClientId, i.Config.ClientSecret, "https://api.adu.microsoft.com/.default")
+ if err != nil {
+ return err
+ }
+ existing, err := i.getDeployment()
+ if err != nil {
+ return nil //Can't read existing deployment, ignore
+ }
+ if existing.UpdateId.Version == deployment.UpdateId.Version && existing.UpdateId.Name == deployment.UpdateId.Name && existing.UpdateId.Provider == deployment.UpdateId.Provider {
+ return azureutils.DeleteADUDeployment(token, i.Config.ADUAccountEndpoint, i.Config.ADUAccountInstance, i.Config.ADUGroup, existing.DeploymentId)
+ }
+ return nil
+}
+func (i *ADUTargetProvider) applyDeployment(deployment azureutils.ADUDeployment) error {
+ token, err := azureutils.GetAzureToken(i.Config.TenantId, i.Config.ClientId, i.Config.ClientSecret, "https://api.adu.microsoft.com/.default")
+ if err != nil {
+ return err
+ }
+ group, err := azureutils.GetADUGroup(token, i.Config.ADUAccountEndpoint, i.Config.ADUAccountInstance, i.Config.ADUGroup)
+ if err != nil {
+ return err
+ }
+ if group.DeploymentId == "" {
+ err = azureutils.CreateADUDeployment(token, i.Config.ADUAccountEndpoint, i.Config.ADUAccountInstance, i.Config.ADUGroup, deployment.DeploymentId, deployment)
+ if err != nil {
+ return err
+ }
+ } else {
+ existing, err := azureutils.GetADUDeployment(token, i.Config.ADUAccountEndpoint, i.Config.ADUAccountInstance, i.Config.ADUGroup, group.DeploymentId)
+ if err != nil {
+ return err
+ }
+ if existing.UpdateId.Version != deployment.UpdateId.Version || existing.UpdateId.Name != deployment.UpdateId.Name || existing.UpdateId.Provider != deployment.UpdateId.Provider {
+ err = azureutils.CreateADUDeployment(token, i.Config.ADUAccountEndpoint, i.Config.ADUAccountInstance, i.Config.ADUGroup, deployment.DeploymentId, deployment)
+ if err != nil {
+ return err
+ }
+ } else {
+ if deployment.IsCanceled {
+ deployment.DeploymentId = existing.DeploymentId
+ err = azureutils.RetryADUDeployment(token, i.Config.ADUAccountEndpoint, i.Config.ADUAccountInstance, i.Config.ADUGroup, deployment.DeploymentId, deployment)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+func (*ADUTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{"update.provider", "update.name", "update.version"},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package iotedge
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "regexp"
+ "strings"
+
+ "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"
+ azureutils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/cloudutils/azure"
+ "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"
+ "github.com/google/uuid"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+const (
+ ENV_NAME string = "SYMPHONY_AGENT_ADDRESS"
+ ENV_SALT string = "SYMPHONY_VERSION_SALT"
+)
+
+// Provider config and type
+type IoTEdgeTargetProviderConfig struct {
+ Name string `json:"name"`
+ KeyName string `json:"keyName"`
+ Key string `json:"key"`
+ IoTHub string `json:"iotHub"`
+ APIVersion string `json:"apiVersion"`
+ DeviceName string `json:"deviceName"`
+ EdgeAgentVersion string `json:"edgeAgentVersion,omitempty"`
+ EdgeHubVersion string `json:"edgeHubVersion,omitempty"`
+}
+type IoTEdgeTargetProvider struct {
+ Config IoTEdgeTargetProviderConfig
+ Context *contexts.ManagerContext
+}
+
+// Azure IoT Edge objects
+type IoTEdgeDeployment struct {
+ ModulesContent map[string]ModuleState `json:"modulesContent"`
+}
+type ModuleState struct {
+ DesiredProperties map[string]interface{} `json:"properties.desired"`
+}
+type DesiredProperties struct {
+ SchemaVersion string `json:"schemaVersion"`
+ Runtime Runtime `json:"runtime"`
+ SystemModules map[string]Module `json:"systemModules"`
+ Modules map[string]Module `json:"modules"`
+ Version int `json:"$version,omitempty"`
+ Metadata interface{} `json:"$metadata,omitempty"`
+}
+type Runtime struct {
+ Type string `json:"type"`
+ Settings map[string]interface{} `json:"settings"`
+}
+type RegistryCredential struct {
+ UserName string `json:"username"`
+ Password string `json:"password"`
+ Address string `json:"address"`
+}
+type Module struct {
+ Type string `json:"type"`
+ Settings map[string]string `json:"settings"`
+ Status string `json:"status,omitempty"`
+ RestartPolicy string `json:"restartPolicy,omitempty"`
+ Version interface{} `json:"version,omitempty"`
+ DesiredProperties map[string]interface{} `json:"metadata,omitempty"`
+ Graph map[string]interface{} `json:"graph,omitempty"`
+ GraphFlavor string `json:"graphFlavor,omitempty"`
+ IotHubRoutes map[string]string `json:"routes,omitempty"`
+ Environments map[string]EnvValue `json:"env,omitempty"`
+}
+type EnvValue struct {
+ Value string `json:"value"`
+}
+type ModuleID struct {
+ ModuleId string `json:"moduleId"`
+}
+type ModuleTwin struct {
+ DeviceId string `json:"deviceId"`
+ ModuleId string `json:"moduleId"`
+ Properties ModuleTwinProperties `json:"properties"`
+ Version interface{} `json:"version"`
+}
+type ModuleTwinProperties struct {
+ Desired map[string]interface{} `json:"desired"`
+ Reported map[string]interface{} `json:"reported"`
+}
+
+func IoTEdgeTargetProviderConfigFromMap(properties map[string]string) (IoTEdgeTargetProviderConfig, error) {
+ ret := IoTEdgeTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["keyName"]; ok {
+ ret.KeyName = v
+ } else {
+ ret.KeyName = "iothubowner"
+ }
+ if v, ok := properties["key"]; ok {
+ ret.Key = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "IoT Edge update provider key is not set", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["iotHub"]; ok {
+ ret.IoTHub = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "IoT Edge update provider IoT Hub name is not set", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["apiVersion"]; ok {
+ ret.APIVersion = v
+ } else {
+ ret.APIVersion = "2020-05-31-preview"
+ }
+ if v, ok := properties["edgeAgentVersion"]; ok {
+ ret.EdgeAgentVersion = v
+ } else {
+ ret.EdgeAgentVersion = "1.3"
+ }
+ if v, ok := properties["edgeHubVersion"]; ok {
+ ret.EdgeHubVersion = v
+ } else {
+ ret.EdgeHubVersion = "1.3"
+ }
+ if v, ok := properties["deviceName"]; ok {
+ ret.DeviceName = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "IoT Edge update provider device name is not set", v1alpha2.BadConfig)
+ }
+ return ret, nil
+}
+
+func (i *IoTEdgeTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := IoTEdgeTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *IoTEdgeTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *IoTEdgeTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("IoT Edge Target Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P(IoT Edge Target): Init()")
+
+ updateConfig, err := toIoTEdgeTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P(IoT Edge Target): expected IoTEdgeTargetProviderConfig: %+v", err)
+ return err
+ }
+ i.Config = updateConfig
+
+ return nil
+}
+func (i *IoTEdgeTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ ctx, span := observability.StartSpan("IoT Edge Target Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P(IoT Edge Target): getting components")
+
+ hubTwin, err := i.getIoTEdgeModuleTwin(ctx, "$edgeHub")
+ if err != nil {
+ sLog.Error(" P(IoT Edge Target): +%v", err)
+ return nil, err
+ }
+
+ modules, err := i.getIoTEdgeModules(ctx)
+ if err != nil {
+ sLog.Error(" P(IoT Edge Target): +%v", err)
+ return nil, err
+ }
+ components := make([]model.ComponentSpec, 0)
+ for k, m := range modules {
+ if k != "$edgeAgent" && k != "$edgeHub" {
+ var twin ModuleTwin
+ twin, err = i.getIoTEdgeModuleTwin(ctx, k)
+ if err != nil {
+ sLog.Error(" P(IoT Edge Target): +%v", err)
+ return nil, err
+ }
+ var component model.ComponentSpec
+ component, err = toComponent(hubTwin, twin, deployment.Instance.Name, m)
+ if err != nil {
+ sLog.Error(" P(IoT Edge Target): +%v", err)
+ return nil, err
+ }
+ components = append(components, component)
+ }
+ }
+
+ return components, nil
+}
+
+func (i *IoTEdgeTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("IoT Edge Target Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P(IoT Edge Target): applying components")
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+
+ edgeAgent, err := i.getIoTEdgeModuleTwin(ctx, "$edgeAgent")
+ if err != nil {
+ sLog.Errorf(" P(IoT Edge Target): +%v", err)
+ return ret, err
+ }
+
+ edgeHub, err := i.getIoTEdgeModuleTwin(ctx, "$edgeHub")
+ if err != nil {
+ sLog.Errorf(" P(IoT Edge Target): +%v", err)
+ return ret, err
+ }
+
+ //updated
+ modules := make(map[string]Module)
+ for _, a := range components {
+ module, e := toModule(a, deployment.Instance.Name, deployment.Instance.Metadata[ENV_NAME], step.Target)
+ if e != nil {
+ ret[a.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: e.Error(),
+ }
+ err = e
+ sLog.Errorf(" P(IoT Edge Target): +%v", err)
+ return ret, err
+ }
+ modules[a.Name] = module
+ }
+ if len(modules) > 0 {
+ err = i.deployToIoTEdge(ctx, deployment.Instance.Name, deployment.Instance.Metadata, modules, edgeAgent, edgeHub)
+ if err != nil {
+ sLog.Errorf(" P(IoT Edge Target): +%v", err)
+ return ret, err
+ }
+ }
+
+ //delete
+ modules = make(map[string]Module)
+ for _, a := range components {
+ module, e := toModule(a, deployment.Instance.Name, deployment.Instance.Metadata[ENV_NAME], step.Target)
+ if e != nil {
+ ret[a.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.DeleteFailed,
+ Message: e.Error(),
+ }
+ return ret, err
+ }
+ modules[a.Name] = module
+ }
+ if len(modules) > 0 {
+ err = i.remvoefromIoTEdge(ctx, deployment.Instance.Name, deployment.Instance.Metadata, modules, edgeAgent, edgeHub)
+ if err != nil {
+ return ret, err
+ }
+ }
+ //TODO: Should we raise events to remove AVA graphs?
+ return ret, nil
+}
+func (*IoTEdgeTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{model.ContainerImage, "container.version", "container.type"},
+ OptionalProperties: []string{"container.restartPolicy", "container.createOptions", "env.*"},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{
+ {Name: "container.restartPolicy", IgnoreCase: false, SkipIfMissing: true},
+ {Name: "container.createOptions", IgnoreCase: false, SkipIfMissing: true},
+ {Name: "container.version", IgnoreCase: false, SkipIfMissing: true},
+ {Name: "container.type", IgnoreCase: false, SkipIfMissing: true},
+ {Name: model.ContainerImage, IgnoreCase: false, SkipIfMissing: true},
+ {Name: "desired.*", IgnoreCase: false, SkipIfMissing: true},
+ {Name: "env.*", IgnoreCase: false, SkipIfMissing: true},
+ },
+ }
+}
+
+func toIoTEdgeTargetProviderConfig(config providers.IProviderConfig) (IoTEdgeTargetProviderConfig, error) {
+ ret := IoTEdgeTargetProviderConfig{}
+ if config == nil {
+ return ret, errors.New("IoTEdgeTargetProviderConfig is null")
+ }
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ if err != nil {
+ return ret, err
+ }
+
+ // ret.IoTHub = providers.LoadEnv(ret.IoTHub)
+ // ret.DeviceName = providers.LoadEnv(ret.DeviceName)
+ // ret.APIVersion = providers.LoadEnv(ret.APIVersion)
+ // ret.KeyName = providers.LoadEnv(ret.KeyName)
+ // ret.Key = providers.LoadEnv(ret.Key)
+
+ if ret.APIVersion == "" {
+ ret.APIVersion = "2020-05-31-preview"
+ }
+ if ret.KeyName == "" {
+ ret.KeyName = "iothubowner"
+ }
+ if ret.EdgeAgentVersion == "" {
+ ret.EdgeAgentVersion = "1.3"
+ }
+ if ret.EdgeHubVersion == "" {
+ ret.EdgeHubVersion = "1.3"
+ }
+ return ret, nil
+}
+
+func toComponent(hubTwin ModuleTwin, twin ModuleTwin, name string, module Module) (model.ComponentSpec, error) {
+ moduleId, _ := reduceKey(twin.ModuleId, name)
+ component := model.ComponentSpec{
+ Name: moduleId,
+ Properties: make(map[string]interface{}),
+ Routes: make([]model.RouteSpec, 0),
+ }
+ for k, v := range module.Environments {
+ if k != ENV_NAME && k != ENV_SALT {
+ component.Properties["env."+k] = v.Value
+ }
+ }
+
+ if v, ok := hubTwin.Properties.Desired["routes"]; ok {
+ routes := v.(map[string]interface{})
+ for k, iv := range routes {
+ def := iv.(string)
+ if strings.Contains(def, "modules/"+twin.ModuleId+"/") { //TODO: this check is not necessarily safe
+ reducedRoute, _ := reduceKey(k, name)
+ reducedDef, _ := replaceKey(def, name)
+ component.Routes = append(component.Routes, model.RouteSpec{
+ Route: reducedRoute,
+ Type: "iothub",
+ Properties: map[string]string{
+ "definition": reducedDef,
+ },
+ })
+ }
+ }
+ }
+
+ component.Properties["container.restartPolicy"] = module.RestartPolicy
+ if module.Version != nil {
+ component.Properties["container.version"] = module.Version.(string)
+ }
+ component.Properties["container.type"] = module.Type
+ if v, ok := module.Settings["createOptions"]; ok {
+ component.Properties["container.createOptions"] = v
+ }
+ if v, ok := module.Settings["image"]; ok {
+ component.Properties[model.ContainerImage] = v
+ }
+ //TODO: We are extracting only keys starting with a lower-case letter here.
+ interestedKey := regexp.MustCompile(`^[a-zA-Z]+`)
+ for k, v := range twin.Properties.Desired { //We are reading desired instead of reported, as we leave IoT Edge state seeking to IoT Edge itself
+ if interestedKey.MatchString(k) {
+ switch v.(type) {
+ case int:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case int8:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case int16:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case int32:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case int64:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case uint:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case uint8:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case uint16:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case uint32:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case uint64:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case float32:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case float64:
+ component.Properties["desired."+k] = fmt.Sprintf("#%v", v)
+ case string:
+ component.Properties["desired."+k] = fmt.Sprintf("%s", v)
+ case bool:
+ component.Properties["desired."+k] = fmt.Sprintf("$%v", v)
+ case []interface{}:
+ data, err := json.Marshal(v)
+ if err == nil {
+ component.Properties["desired."+k] = string(data)
+ } else {
+ component.Properties["desired."+k] = fmt.Sprintf("%v", v) //The "desired." prefix is added to match with what's generated during Apply
+ }
+ default:
+ data, err := json.Marshal(v)
+ if err == nil {
+ component.Properties["desired."+k] = string(data)
+ } else {
+ component.Properties["desired."+k] = fmt.Sprintf("%v", v) //The "desired." prefix is added to match with what's generated during Apply
+ }
+ }
+ }
+ }
+ return component, nil
+}
+func readProperty(properties map[string]interface{}, key string, defaultVal string, required bool) (string, error) {
+ if v, ok := properties[key]; ok && v != "" {
+ return fmt.Sprintf("%v", v), nil
+ }
+ if required && defaultVal == "" {
+ return "", v1alpha2.NewCOAError(nil, fmt.Sprintf("required property '%s' is missng", key), v1alpha2.BadRequest)
+ }
+ return defaultVal, nil
+}
+func toModule(component model.ComponentSpec, name string, agentName string, targetName string) (Module, error) {
+ policy, err := readProperty(component.Properties, "container.restartPolicy", "always", false)
+ if err != nil {
+ return Module{}, err
+ }
+ createOptions, err := readProperty(component.Properties, "container.createOptions", "", false)
+ if err != nil {
+ return Module{}, err
+ }
+ version, err := readProperty(component.Properties, "container.version", "", true)
+ if err != nil {
+ return Module{}, err
+ }
+ componentType, err := readProperty(component.Properties, "container.type", "", true)
+ if err != nil {
+ return Module{}, err
+ }
+ image, err := readProperty(component.Properties, model.ContainerImage, "", true)
+ if err != nil {
+ return Module{}, err
+ }
+ module := Module{
+ Version: version,
+ Type: componentType,
+ RestartPolicy: policy,
+ Status: "running",
+ Settings: map[string]string{
+ "image": image,
+ "createOptions": createOptions,
+ },
+ }
+ module.DesiredProperties = make(map[string]interface{})
+ module.Graph = make(map[string]interface{})
+ module.GraphFlavor = "ava"
+ module.IotHubRoutes = make(map[string]string)
+ module.Environments = make(map[string]EnvValue)
+ for k, v := range component.Properties {
+ // TODO: Transition from map[string]string to map[string]interface{}
+ // for now we would only do this for string properties
+ if sv, ok := v.(string); ok {
+ tv := utils.ProjectValue(sv, name)
+ if strings.HasPrefix(k, "desired.") {
+ module.DesiredProperties[k[8:]] = tv
+ // } else if strings.HasPrefix(k, "graph.") {
+ // if k == "graph.methodFlavor" {
+ // module.GraphFlavor = v
+ // } else {
+ // module.Graph[k[6:]] = v
+ // }
+ } else if strings.HasPrefix(k, "env.") {
+ module.Environments[k[4:]] = EnvValue{Value: tv}
+ }
+ }
+ }
+
+ module.Environments[ENV_SALT] = EnvValue{Value: uuid.New().String()}
+
+ if agentName != "" {
+ module.Environments[ENV_NAME] = EnvValue{Value: fmt.Sprintf("%s-%s-%s", "target-runtime", targetName, agentName)}
+ }
+ for _, v := range component.Routes {
+ if v.Type == "iothub" {
+ module.IotHubRoutes[v.Route] = v.Properties["definition"]
+ }
+ }
+
+ return module, nil
+}
+func (i *IoTEdgeTargetProvider) getIoTEdgeModuleTwin(ctx context.Context, id string) (ModuleTwin, error) {
+ url := fmt.Sprintf("https://%s/twins/%s/modules/%s?api-version=%s", i.Config.IoTHub, i.Config.DeviceName, id, i.Config.APIVersion)
+ _, span := observability.StartSpan("IoT Edge REST API", ctx, &map[string]string{
+ "method": "getIoTEdgeModuleTwin",
+ "url": url,
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ module := ModuleTwin{}
+ sasToken := azureutils.CreateSASToken(fmt.Sprintf("%s/devices/%s", i.Config.IoTHub, i.Config.DeviceName), i.Config.KeyName, i.Config.Key)
+ client := &http.Client{}
+ req, err := http.NewRequest(http.MethodGet, url, nil)
+ if err != nil {
+ sLog.Errorf("failed to get IoT Edge modules: %v", err)
+ return module, v1alpha2.NewCOAError(err, "failed to get IoT Edge modules", v1alpha2.InternalError)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", sasToken)
+ resp, err := client.Do(req)
+ if err != nil {
+ sLog.Errorf("failed to get IoT Edge modules: %v", err)
+ return module, v1alpha2.NewCOAError(err, "failed to get IoT Edge modules", v1alpha2.InternalError)
+ }
+ if resp.StatusCode != http.StatusOK {
+ sLog.Errorf("failed to get IoT Edge modules: %v", resp)
+ //return module, v1alpha1.NewCOAError(nil, "failed to get IoT Edge modules", v1alpha1.InternalError) //TODO: carry over HTTP status code
+ }
+ defer resp.Body.Close()
+ bodyBytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ sLog.Errorf("failed to get IoT Edge modules: %v", err)
+ return module, v1alpha2.NewCOAError(err, "failed to get IoT Edge modules", v1alpha2.InternalError)
+ }
+ err = json.Unmarshal(bodyBytes, &module)
+ if err != nil {
+ sLog.Errorf("failed to get IoT Edge modules: %v", err)
+ return module, v1alpha2.NewCOAError(err, "failed to get IoT Edge modules", v1alpha2.InternalError)
+ }
+ return module, nil
+}
+func (i *IoTEdgeTargetProvider) getIoTEdgeModules(ctx context.Context) (map[string]Module, error) {
+ ret := make(map[string]Module)
+ agentTwin, err := i.getIoTEdgeModuleTwin(ctx, "$edgeAgent")
+ if err != nil {
+ return ret, err
+ }
+ data, err := json.Marshal(agentTwin.Properties.Desired["modules"])
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ if err != nil {
+ return ret, err
+ }
+
+ return ret, nil
+}
+
+func (i *IoTEdgeTargetProvider) remvoefromIoTEdge(ctx context.Context, name string, metadata map[string]string, modules map[string]Module, agentRef ModuleTwin, hubRef ModuleTwin) error {
+ deployment := makeDefaultDeployment(metadata, i.Config.EdgeAgentVersion, i.Config.EdgeHubVersion)
+ err := reduceDeployment(&deployment, name, modules, agentRef, hubRef)
+ if err != nil {
+ return err
+ }
+ return i.applyIoTEdgeDeployment(ctx, deployment)
+}
+
+func (i *IoTEdgeTargetProvider) deployToIoTEdge(ctx context.Context, name string, metadata map[string]string, modules map[string]Module, agentRef ModuleTwin, hubRef ModuleTwin) error {
+
+ deployment := makeDefaultDeployment(metadata, i.Config.EdgeAgentVersion, i.Config.EdgeHubVersion)
+
+ err := updateDeployment(&deployment, name, modules, agentRef, hubRef)
+ if err != nil {
+ return err
+ }
+ return i.applyIoTEdgeDeployment(ctx, deployment)
+}
+
+func (i *IoTEdgeTargetProvider) applyIoTEdgeDeployment(ctx context.Context, deployment IoTEdgeDeployment) error {
+ url := fmt.Sprintf("https://%s/devices/%s/applyConfigurationContent?api-version=%s", i.Config.IoTHub, i.Config.DeviceName, i.Config.APIVersion)
+ _, span := observability.StartSpan("IoT Edge REST API", ctx, &map[string]string{
+ "method": "applyIoTEdgeDeployment",
+ "url": url,
+ })
+
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sasToken := azureutils.CreateSASToken(fmt.Sprintf("%s/devices/%s", i.Config.IoTHub, i.Config.DeviceName), i.Config.KeyName, i.Config.Key)
+ client := &http.Client{}
+ payload, err := json.Marshal(deployment)
+ if err != nil {
+ return v1alpha2.NewCOAError(err, "failed to serialize IoT Edge deployemnt", v1alpha2.SerializationError)
+ }
+
+ req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payload))
+ if err != nil {
+ sLog.Errorf("failed to post IoT Edge deployment: %v", err)
+ return v1alpha2.NewCOAError(err, "failed to post IoT Edge deployment", v1alpha2.InternalError)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Authorization", sasToken)
+ resp, err := client.Do(req)
+ if err != nil {
+ sLog.Errorf("failed to post IoT Edge deployment: %v", err)
+ return v1alpha2.NewCOAError(err, "failed to post IoT Edge deployment", v1alpha2.InternalError)
+ }
+ if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
+ sLog.Errorf("failed to post IoT Edge deployment: %v", resp)
+ return v1alpha2.NewCOAError(nil, "failed to post IoT Edge deployment", v1alpha2.InternalError) //TODO: carry over HTTP status code
+ }
+ return nil
+}
+
+func replaceKey(key string, name string) (string, bool) {
+ if name != "" && strings.Contains(key, name+"-") {
+ return strings.ReplaceAll(key, name+"-", ""), true
+ }
+ return key, false
+}
+
+func reduceKey(key string, name string) (string, bool) {
+ if name != "" && strings.HasPrefix(key, name+"-") {
+ return key[len(name)+1:], true
+ }
+ return key, false
+}
+func expandKey(key string, name string) string {
+ if name != "" {
+ return name + "-" + key
+ }
+ return key
+}
+
+func carryOverRoutes(deployment *IoTEdgeDeployment, ref ModuleTwin) {
+ if ref.ModuleId != "" {
+ if v, ok := ref.Properties.Desired["routes"]; ok {
+ if vc, ok := v.(map[string]string); ok {
+ m := deployment.ModulesContent["$edgeHub"].DesiredProperties["routes"].(map[string]string)
+ for k, iv := range vc {
+ m[k] = iv
+ }
+ }
+ }
+ }
+}
+
+func updateDeployment(deployment *IoTEdgeDeployment, name string, modules map[string]Module, agentRef ModuleTwin, hubRef ModuleTwin) error {
+
+ // add all other modules that are not in the current module list so that we can write them back
+ otherModules := map[string]bool{}
+ if agentRef.ModuleId != "" {
+ carryOverRoutes(deployment, agentRef)
+ im, ok := agentRef.Properties.Desired["modules"].(map[string]interface{})
+ if ok {
+ for k, _ := range im {
+ rk, reduced := reduceKey(k, name)
+ if !reduced {
+ strContent, _ := json.Marshal(im[k])
+ mRef := Module{}
+ err := json.Unmarshal(strContent, &mRef)
+ if err != nil {
+ return err
+ }
+ modules[rk] = mRef
+ otherModules[rk] = true
+ }
+ }
+ }
+ }
+
+ // create a new module collection
+ deployment.ModulesContent["$edgeAgent"].DesiredProperties["modules"] = make(map[string]Module)
+
+ rd := deployment.ModulesContent["$edgeHub"].DesiredProperties["routes"].(map[string]string)
+
+ if v, ok := hubRef.Properties.Desired["routes"]; ok {
+ routes := v.(map[string]interface{})
+ for ik, iv := range routes {
+ rd[ik] = iv.(string)
+ }
+ }
+
+ // add all modules, wich include modules from current deployment as well as other modules
+ for k, m := range modules {
+ d := deployment.ModulesContent["$edgeAgent"].DesiredProperties["modules"].(map[string]Module)
+ ek := k
+ if _, ok := otherModules[k]; !ok {
+ ek = expandKey(k, name)
+ }
+ d[ek] = m
+ if len(m.DesiredProperties) > 0 {
+ deployment.ModulesContent[ek] = ModuleState{
+ DesiredProperties: map[string]interface{}{},
+ }
+ for ik, iv := range m.DesiredProperties {
+ deployment.ModulesContent[ek].DesiredProperties[ik] = iv
+ }
+ }
+ if len(m.IotHubRoutes) > 0 {
+ if _, ok := otherModules[k]; !ok {
+ for rk, rv := range m.IotHubRoutes {
+ rek := expandKey(rk, name)
+ mrv := modifyRoutes(rv, name, modules, otherModules)
+ rd[rek] = mrv
+ }
+ }
+ }
+ }
+ return nil
+}
+func modifyRoutes(route string, name string, modules map[string]Module, otherModules map[string]bool) string {
+ for k, _ := range modules {
+ if _, ok := otherModules[k]; !ok {
+ route = strings.ReplaceAll(route, "modules/"+k, "modules/"+name+"-"+k)
+ }
+ }
+ return route
+}
+
+func reduceDeployment(deployment *IoTEdgeDeployment, name string, modules map[string]Module, ref ModuleTwin, hubRef ModuleTwin) error {
+
+ otherModules := map[string]bool{}
+
+ rd := deployment.ModulesContent["$edgeHub"].DesiredProperties["routes"].(map[string]string)
+
+ if v, ok := hubRef.Properties.Desired["routes"]; ok {
+ routes := v.(map[string]interface{})
+ for ik, iv := range routes {
+ rd[ik] = iv.(string)
+ }
+ }
+
+ if ref.ModuleId != "" {
+ carryOverRoutes(deployment, ref)
+ im, ok := ref.Properties.Desired["modules"].(map[string]interface{})
+ if ok {
+ for k, _ := range im {
+ rk, reduced := reduceKey(k, name)
+ if !reduced {
+ strContent, _ := json.Marshal(im[k])
+ mRef := Module{}
+ err := json.Unmarshal(strContent, &mRef)
+ if err != nil {
+ return err
+ }
+ modules[rk] = mRef
+ otherModules[rk] = true
+ } else {
+ if len(modules[rk].IotHubRoutes) > 0 {
+ for ik, _ := range modules[rk].IotHubRoutes {
+ delete(rd, expandKey(ik, name))
+ }
+ }
+ delete(modules, rk)
+ }
+ }
+ }
+ }
+
+ deployment.ModulesContent["$edgeAgent"].DesiredProperties["modules"] = make(map[string]Module)
+ for k, m := range modules {
+ d := deployment.ModulesContent["$edgeAgent"].DesiredProperties["modules"].(map[string]Module)
+ ek := k
+ if _, ok := otherModules[k]; !ok {
+ ek = expandKey(k, name)
+ }
+ d[ek] = m
+ if len(m.DesiredProperties) > 0 {
+ deployment.ModulesContent[ek] = ModuleState{
+ DesiredProperties: map[string]interface{}{},
+ }
+ for ik, iv := range m.DesiredProperties {
+ deployment.ModulesContent[ek].DesiredProperties[ik] = iv
+ }
+ }
+ }
+ return nil
+}
+
+func makeDefaultDeployment(metadata map[string]string, edgeAgentVersion string, edgeHubVersion string) IoTEdgeDeployment {
+
+ deployment := IoTEdgeDeployment{
+ ModulesContent: map[string]ModuleState{
+ "$edgeAgent": {
+ DesiredProperties: map[string]interface{}{
+ "schemaVersion": "1.0",
+ "runtime": Runtime{
+ Type: "docker",
+ Settings: map[string]interface{}{
+ "minDockerVersion": "v1.25",
+ "loggingOption": "",
+ },
+ },
+ "systemModules": map[string]Module{
+ "edgeAgent": Module{
+ Type: "docker",
+ Settings: map[string]string{
+ "image": "mcr.microsoft.com/azureiotedge-agent:" + edgeAgentVersion,
+ "createOptions": "",
+ },
+ },
+ "edgeHub": {
+ Type: "docker",
+ RestartPolicy: "always",
+ Status: "running",
+ Settings: map[string]string{
+ "image": "mcr.microsoft.com/azureiotedge-hub:" + edgeHubVersion,
+ "createOptions": "{\"HostConfig\":{\"PortBindings\":{\"5671/tcp\":[{\"HostPort\":\"5671\"}],\"8883/tcp\":[{\"HostPort\":\"8883\"}],\"443/tcp\":[{\"HostPort\":\"443\"}]}}}",
+ },
+ },
+ },
+ },
+ },
+ "$edgeHub": {
+ DesiredProperties: map[string]interface{}{
+ "schemaVersion": "1.0",
+ "routes": map[string]string{},
+ "storeAndForwardConfiguration": map[string]int{ //TODO: this is also a hack
+ "timeToLiveSecs": 7200,
+ },
+ },
+ },
+ },
+ }
+ if v, ok := metadata["$edgeAgent.registryCredentials"]; ok && strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
+ credentials := make(map[string]RegistryCredential)
+ data := []byte(v)
+ err := json.Unmarshal(data, &credentials)
+ if err == nil {
+ (deployment.ModulesContent["$edgeAgent"].DesiredProperties["runtime"].(Runtime)).Settings["registryCredentials"] = credentials
+ }
+ }
+ return deployment
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package configmap
+
+import (
+ "context"
+ "encoding/json"
+ "path/filepath"
+ "strconv"
+
+ "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"
+ "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"
+ corev1 "k8s.io/api/core/v1"
+ kerrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
+ "k8s.io/client-go/discovery"
+ "k8s.io/client-go/discovery/cached/memory"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/restmapper"
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/util/homedir"
+)
+
+var (
+ decUnstructured = yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
+ sLog = logger.NewLogger("coa.runtime")
+)
+
+type (
+ // ConfigMapTargetProviderConfig is the configuration for the kubectl target provider
+ ConfigMapTargetProviderConfig struct {
+ Name string `json:"name,omitempty"`
+ ConfigType string `json:"configType,omitempty"`
+ ConfigData string `json:"configData,omitempty"`
+ Context string `json:"context,omitempty"`
+ InCluster bool `json:"inCluster"`
+ }
+
+ // ConfigMapTargetProvider is the kubectl target provider
+ ConfigMapTargetProvider struct {
+ Config ConfigMapTargetProviderConfig
+ Context *contexts.ManagerContext
+ Client kubernetes.Interface
+ DynamicClient dynamic.Interface
+ DiscoveryClient *discovery.DiscoveryClient
+ Mapper *restmapper.DeferredDiscoveryRESTMapper
+ RESTConfig *rest.Config
+ }
+)
+
+// ConfigMapTargetProviderConfigFromMap converts a map to a ConfigMapTargetProviderConfig
+func ConfigMapTargetProviderConfigFromMap(properties map[string]string) (ConfigMapTargetProviderConfig, error) {
+ ret := ConfigMapTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["configType"]; ok {
+ ret.ConfigType = v
+ }
+ if v, ok := properties["configData"]; ok {
+ ret.ConfigData = v
+ }
+ if v, ok := properties["context"]; ok {
+ ret.Context = v
+ }
+ if v, ok := properties["inCluster"]; ok {
+ val := v
+ if val != "" {
+ bVal, err := strconv.ParseBool(val)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid bool value in the 'inCluster' setting of kubectl provider", v1alpha2.BadConfig)
+ }
+ ret.InCluster = bVal
+ }
+ }
+ return ret, nil
+}
+
+func (s *ConfigMapTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+// InitWithMap initializes the configmap target provider with a map
+func (i *ConfigMapTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := ConfigMapTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+
+ return i.Init(config)
+}
+
+// Init initializes the configmap target provider
+func (i *ConfigMapTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan(
+ "ConfigMap Target Provider",
+ context.TODO(),
+ &map[string]string{
+ "method": "Init",
+ },
+ )
+ var err error = nil
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Info(" P (ConfigMap Target): Init()")
+
+ updateConfig, err := toConfigMapTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (ConfigMap Target): expected ConfigMapTargetProviderConfig - %+v", err)
+ return err
+ }
+
+ i.Config = updateConfig
+ var kConfig *rest.Config
+ if i.Config.InCluster {
+ kConfig, err = rest.InClusterConfig()
+ } else {
+ switch i.Config.ConfigType {
+ case "path":
+ if i.Config.ConfigData == "" {
+ if home := homedir.HomeDir(); home != "" {
+ i.Config.ConfigData = filepath.Join(home, ".kube", "config")
+ } else {
+ err = v1alpha2.NewCOAError(nil, "can't locate home direction to read default kubernetes config file, to run in cluster, set inCluster config setting to true", v1alpha2.BadConfig)
+ sLog.Errorf(" P (ConfigMap Target): %+v", err)
+ return err
+ }
+ }
+ kConfig, err = clientcmd.BuildConfigFromFlags("", i.Config.ConfigData)
+ case "inline":
+ if i.Config.ConfigData != "" {
+ kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData))
+ if err != nil {
+ sLog.Errorf(" P (ConfigMap Target): %+v", err)
+ return err
+ }
+ } else {
+ err = v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig)
+ sLog.Errorf(" P (ConfigMap Target): %+v", err)
+ return err
+ }
+ default:
+ err = v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and inline", v1alpha2.BadConfig)
+ sLog.Errorf(" P (ConfigMap Target): %+v", err)
+ return err
+ }
+ }
+ if err != nil {
+ sLog.Errorf(" P (ConfigMap Target): %+v", err)
+ return err
+ }
+
+ i.Client, err = kubernetes.NewForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (ConfigMap Target): %+v", err)
+ return err
+ }
+
+ i.DynamicClient, err = dynamic.NewForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (ConfigMap Target): %+v", err)
+ return err
+ }
+
+ i.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (ConfigMap Target): %+v", err)
+ return err
+ }
+
+ i.Mapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(i.DiscoveryClient))
+ i.RESTConfig = kConfig
+ return nil
+}
+
+// toConfigMapTargetProviderConfig converts a generic IProviderConfig to a ConfigMapTargetProviderConfig
+func toConfigMapTargetProviderConfig(config providers.IProviderConfig) (ConfigMapTargetProviderConfig, error) {
+ ret := ConfigMapTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+// Get gets the artifacts for a configmap
+func (i *ConfigMapTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ ctx, span := observability.StartSpan(
+ "ConfigMap Target Provider",
+ ctx, &map[string]string{
+ "method": "Get",
+ },
+ )
+ var err error = nil
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (ConfigMap Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ ret := make([]model.ComponentSpec, 0)
+ for _, component := range references {
+ var obj *corev1.ConfigMap
+ obj, err = i.Client.CoreV1().ConfigMaps(deployment.Instance.Scope).Get(ctx, component.Component.Name, metav1.GetOptions{})
+ if err != nil {
+ if kerrors.IsNotFound(err) {
+ sLog.Infof(" P (ConfigMap Target): resource not found: %s", err)
+ continue
+ }
+ sLog.Error(" P (ConfigMap Target): failed to read object: +%v", err)
+ return nil, err
+ }
+ component.Component.Properties = make(map[string]interface{})
+ for key, value := range obj.Data {
+ var data interface{}
+ err = json.Unmarshal([]byte(value), &data)
+ if err == nil {
+ component.Component.Properties[key] = data
+ } else {
+ component.Component.Properties[key] = value
+ }
+ }
+ ret = append(ret, component.Component)
+ }
+
+ return ret, nil
+}
+
+// Apply applies the configmap artifacts
+func (i *ConfigMapTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan(
+ "ConfigMap Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "Apply",
+ },
+ )
+ var err error = nil
+ defer utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof(" P (ConfigMap Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+ components = step.GetUpdatedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Type == "config" {
+ newConfigMap := &corev1.ConfigMap{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: component.Name,
+ Namespace: deployment.Instance.Scope,
+ },
+ Data: make(map[string]string),
+ }
+ for key, value := range component.Properties {
+ if v, ok := value.(string); ok {
+ newConfigMap.Data[key] = v
+ } else {
+ jData, _ := json.Marshal(value)
+ newConfigMap.Data[key] = string(jData)
+ }
+ }
+ i.ensureNamespace(ctx, deployment.Instance.Scope)
+ err = i.applyConfigMap(ctx, newConfigMap, deployment.Instance.Scope)
+ if err != nil {
+ sLog.Error(" P (ConfigMap Target): failed to apply configmap: +%v", err)
+ return ret, err
+ }
+ }
+ }
+ }
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Type == "config" {
+ err = i.deleteConfigMap(ctx, component.Name, deployment.Instance.Scope)
+ if err != nil {
+ sLog.Error(" P (ConfigMap Target): failed to delete configmap: +%v", err)
+ return ret, err
+ }
+ }
+ }
+ }
+ return ret, nil
+}
+
+// ensureNamespace ensures that the namespace exists
+func (k *ConfigMapTargetProvider) ensureNamespace(ctx context.Context, namespace string) error {
+ ctx, span := observability.StartSpan(
+ "ConfigMap Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "ensureNamespace",
+ },
+ )
+ var err error = nil
+ defer utils.CloseSpanWithError(span, &err)
+
+ _, err = k.Client.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
+ if err == nil {
+ return nil
+ }
+
+ if kerrors.IsNotFound(err) {
+ _, err = k.Client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: namespace,
+ },
+ }, metav1.CreateOptions{})
+ if err != nil {
+ sLog.Error(" P (ConfigMap Target): failed to create namespace: +%v", err)
+ return err
+ }
+
+ } else {
+ sLog.Error(" P (ConfigMap Target): failed to get namespace: +%v", err)
+ return err
+ }
+
+ return nil
+}
+
+// GetValidationRule returns validation rule for the provider
+func (*ConfigMapTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{
+ {
+ Name: "*", //react to all property changes
+ },
+ },
+ }
+}
+
+// deleteConfigMap deletes a configmap
+func (i *ConfigMapTargetProvider) deleteConfigMap(ctx context.Context, name string, scope string) error {
+ err := i.Client.CoreV1().ConfigMaps(scope).Delete(ctx, name, metav1.DeleteOptions{})
+ if err != nil {
+ if !kerrors.IsNotFound(err) {
+ sLog.Error(" P (Kubectl Target): failed to delete configmap: +%v", err)
+ return err
+ }
+ }
+ return nil
+}
+
+// applyCustomResource applies a custom resource from a byte array
+func (i *ConfigMapTargetProvider) applyConfigMap(ctx context.Context, config *corev1.ConfigMap, scope string) error {
+ existingConfigMap, err := i.Client.CoreV1().ConfigMaps(scope).Get(ctx, config.Name, metav1.GetOptions{})
+ if err != nil {
+ if kerrors.IsNotFound(err) {
+ sLog.Infof(" P (ConfigMap Target): resource not found: %s", err)
+ _, err = i.Client.CoreV1().ConfigMaps(scope).Create(ctx, config, metav1.CreateOptions{})
+ if err != nil {
+ sLog.Error(" P (ConfigMap Target): failed to create configmap: +%v", err)
+ return err
+ }
+ return nil
+ }
+ sLog.Error(" P (ConfigMap Target): failed to read object: +%v", err)
+ return err
+ }
+
+ existingConfigMap.Data = config.Data
+
+ _, err = i.Client.CoreV1().ConfigMaps(scope).Update(ctx, existingConfigMap, metav1.UpdateOptions{})
+ if err != nil {
+ sLog.Error(" P (ConfigMap Target): failed to update configmap: +%v", err)
+ return err
+ }
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package conformance
+
+import (
+ "context"
+ "testing"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+ "github.com/stretchr/testify/assert"
+)
+
+func RequiredPropertiesAndMetadata[P target.ITargetProvider](t *testing.T, p P) {
+ desired := []model.ComponentSpec{
+ {
+ Name: "test-1",
+ Properties: map[string]interface{}{},
+ Metadata: map[string]string{},
+ },
+ }
+
+ step := model.DeploymentStep{
+ Components: []model.ComponentStep{
+ {
+ Component: model.ComponentSpec{
+ Name: "test-1",
+ Properties: map[string]interface{}{},
+ Metadata: map[string]string{},
+ },
+ },
+ },
+ }
+
+ rule := p.GetValidationRule(context.Background())
+
+ for _, property := range rule.RequiredProperties {
+ desired[0].Properties[property] = "dummy property"
+ step.Components[0].Component.Properties[property] = "dummy property"
+ }
+
+ for _, metadata := range rule.RequiredMetadata {
+ desired[0].Metadata[metadata] = "dummy metadata"
+ step.Components[0].Component.Metadata[metadata] = "dummy metadata"
+ }
+
+ deployment := model.DeploymentSpec{
+ Solution: model.SolutionSpec{
+ Components: desired,
+ },
+ ComponentStartIndex: 0,
+ ComponentEndIndex: 1,
+ }
+ _, err := p.Apply(context.Background(), deployment, step, true)
+ assert.Nil(t, err)
+}
+func AnyRequiredPropertiesMissing[P target.ITargetProvider](t *testing.T, p P) {
+
+ desired := []model.ComponentSpec{
+ {
+ Name: "test-1",
+ Properties: map[string]interface{}{},
+ Metadata: map[string]string{},
+ },
+ }
+
+ step := model.DeploymentStep{
+ Components: []model.ComponentStep{
+ {
+ Component: model.ComponentSpec{
+ Name: "test-1",
+ Properties: map[string]interface{}{},
+ Metadata: map[string]string{},
+ },
+ },
+ },
+ }
+
+ rule := p.GetValidationRule(context.Background())
+
+ for _, metadata := range rule.RequiredMetadata {
+ desired[0].Metadata[metadata] = "dummy metadata"
+ }
+
+ for i, _ := range rule.RequiredProperties {
+ desired[0].Properties = make(map[string]interface{}, len(rule.RequiredProperties)-1)
+ slice := append(append([]string{}, rule.RequiredProperties[:i]...), rule.RequiredProperties[i+1:]...)
+ for _, property := range slice {
+ desired[0].Properties[property] = "dummy property"
+ }
+ deployment := model.DeploymentSpec{
+ Solution: model.SolutionSpec{
+ Components: desired,
+ },
+ ComponentStartIndex: 0,
+ ComponentEndIndex: 1,
+ }
+ _, err := p.Apply(context.Background(), deployment, step, true)
+ assert.NotNil(t, err)
+ coaErr := err.(v1alpha2.COAError)
+ assert.Equal(t, v1alpha2.BadRequest, coaErr.State)
+ }
+}
+func ConformanceSuite[P target.ITargetProvider](t *testing.T, p P) {
+ t.Run("Level=Basic", func(t *testing.T) {
+ RequiredPropertiesAndMetadata(t, p)
+ AnyRequiredPropertiesMissing(t, p)
+ })
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package docker
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "strings"
+
+ "github.com/docker/docker/api/types"
+ "github.com/docker/docker/api/types/container"
+ "github.com/docker/docker/client"
+ "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"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type DockerTargetProviderConfig struct {
+ Name string `json:"name"`
+}
+
+type DockerTargetProvider struct {
+ Config DockerTargetProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func DockerTargetProviderConfigFromMap(properties map[string]string) (DockerTargetProviderConfig, error) {
+ ret := DockerTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ return ret, nil
+}
+func (d *DockerTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := DockerTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return d.Init(config)
+}
+func (s *DockerTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (d *DockerTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("Docker Target Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (Docker Target): Init()")
+
+ // convert config to DockerTargetProviderConfig type
+ dockerConfig, err := toDockerTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (Docker Target): expected DockerTargetProviderConfig: %+v", err)
+ return err
+ }
+
+ d.Config = dockerConfig
+ return nil
+}
+func toDockerTargetProviderConfig(config providers.IProviderConfig) (DockerTargetProviderConfig, error) {
+ ret := DockerTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+func (i *DockerTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ ctx, span := observability.StartSpan("Docker Target Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof(" P (Docker Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ cli, err := client.NewClientWithOpts(client.FromEnv)
+ if err != nil {
+ sLog.Errorf(" P (Docker Target): failed to create docker client: %+v", err)
+ return nil, err
+ }
+
+ ret := make([]model.ComponentSpec, 0)
+ for _, component := range references {
+ var info types.ContainerJSON
+ info, err = cli.ContainerInspect(ctx, component.Component.Name)
+ if err == nil {
+ name := info.Name
+ if len(name) > 0 && name[0] == '/' {
+ name = name[1:]
+ }
+ component := model.ComponentSpec{
+ Name: name,
+ Properties: make(map[string]interface{}),
+ }
+ // container.args
+ if len(info.Args) > 0 {
+ argsData, _ := json.Marshal(info.Args)
+ component.Properties["container.args"] = string(argsData)
+ }
+ // container.image
+ component.Properties[model.ContainerImage] = info.Config.Image
+ if info.HostConfig != nil {
+ resources, _ := json.Marshal(info.HostConfig.Resources)
+ component.Properties["container.resources"] = string(resources)
+ }
+ // container.ports
+ if info.NetworkSettings != nil && len(info.NetworkSettings.Ports) > 0 {
+ ports, _ := json.Marshal(info.NetworkSettings.Ports)
+ component.Properties["container.ports"] = string(ports)
+ }
+ // container.cmd
+ if len(info.Config.Cmd) > 0 {
+ cmdData, _ := json.Marshal(info.Config.Cmd)
+ component.Properties["container.commands"] = string(cmdData)
+ }
+ // container.volumeMounts
+ if len(info.Mounts) > 0 {
+ volumeData, _ := json.Marshal(info.Mounts)
+ component.Properties["container.volumeMounts"] = string(volumeData)
+ }
+ // get environment varibles that are passed in by the reference
+ env := info.Config.Env
+ if len(env) > 0 {
+ for _, e := range env {
+ pair := strings.Split(e, "=")
+ if len(pair) == 2 {
+ for _, s := range references {
+ if s.Component.Name == component.Name {
+ for k, _ := range s.Component.Properties {
+ if k == "env."+pair[0] {
+ component.Properties[k] = pair[1]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ret = append(ret, component)
+ }
+ }
+
+ return ret, nil
+}
+
+func (i *DockerTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("Docker Target Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof(" P (Docker Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ injections := &model.ValueInjections{
+ InstanceId: deployment.Instance.Name,
+ SolutionId: deployment.Instance.Solution,
+ TargetId: deployment.ActiveTarget,
+ }
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+
+ cli, err := client.NewClientWithOpts(client.FromEnv)
+ if err != nil {
+ sLog.Errorf(" P (Docker Target): failed to create docker client: %+v", err)
+ return ret, err
+ }
+
+ for _, component := range step.Components {
+ if component.Action == "update" {
+ image := model.ReadPropertyCompat(component.Component.Properties, model.ContainerImage, injections)
+ resources := model.ReadPropertyCompat(component.Component.Properties, "container.resources", injections)
+ if image == "" {
+ err = errors.New("component doesn't have container.image property")
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P (Helm Target): component doesn't have container.image property")
+ return ret, err
+ }
+
+ alreadyRunning := true
+ _, err = cli.ContainerInspect(ctx, component.Component.Name)
+ if err != nil { //TODO: check if the error is ErrNotFound
+ alreadyRunning = false
+ }
+
+ // TODO: I don't think we need to do an explict image pull here, as Docker will pull the image upon cache miss
+ // reader, err := cli.ImagePull(ctx, image, types.ImagePullOptions{})
+ // if err != nil {
+ // observ_utils.CloseSpanWithError(span, &err)
+ // sLog.Errorf(" P (Docker Target): failed to pull docker image: %+v", err)
+ // return err
+ // }
+
+ // defer reader.Close()
+ // io.Copy(os.Stdout, reader)
+
+ if alreadyRunning {
+ err = cli.ContainerStop(context.TODO(), component.Component.Name, nil)
+ if err != nil {
+ if !client.IsErrNotFound(err) {
+ sLog.Errorf(" P (Docker Target): failed to stop a running container: %+v", err)
+ return ret, err
+ }
+ }
+ err = cli.ContainerRemove(context.TODO(), component.Component.Name, types.ContainerRemoveOptions{})
+ if err != nil {
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P (Docker Target): failed to remove existing container: %+v", err)
+ return ret, err
+ }
+ }
+
+ // prepare environment variables
+ env := make([]string, 0)
+ for k, v := range component.Component.Properties {
+ if strings.HasPrefix(k, "env.") {
+ env = append(env, strings.TrimPrefix(k, "env.")+"="+v.(string))
+ }
+ }
+
+ containerConfig := container.Config{
+ Image: image,
+ Env: env,
+ }
+ var hostConfig *container.HostConfig
+ if resources != "" {
+ var resourceSpec container.Resources
+ err = json.Unmarshal([]byte(resources), &resourceSpec)
+ if err != nil {
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P (Docker Target): failed to read container resource settings: %+v", err)
+ return ret, err
+ }
+ hostConfig = &container.HostConfig{
+ Resources: resourceSpec,
+ }
+ }
+ var container container.ContainerCreateCreatedBody
+ container, err = cli.ContainerCreate(context.TODO(), &containerConfig, hostConfig, nil, nil, component.Component.Name)
+ if err != nil {
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P (Docker Target): failed to create container: %+v", err)
+ return ret, err
+ }
+
+ if err = cli.ContainerStart(context.TODO(), container.ID, types.ContainerStartOptions{}); err != nil {
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P (Docker Target): failed to start container: %+v", err)
+ return ret, err
+ }
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.Updated,
+ Message: "",
+ }
+ } else {
+ err = cli.ContainerStop(context.TODO(), component.Component.Name, nil)
+ if err != nil {
+ if !client.IsErrNotFound(err) {
+ sLog.Errorf(" P (Docker Target): failed to stop a running container: %+v", err)
+ return ret, err
+ }
+ }
+ err = cli.ContainerRemove(context.TODO(), component.Component.Name, types.ContainerRemoveOptions{})
+ if err != nil {
+ if !client.IsErrNotFound(err) {
+ sLog.Errorf(" P (Docker Target): failed to remove existing container: %+v", err)
+ return ret, err
+ }
+ }
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.Deleted,
+ Message: "",
+ }
+ }
+ }
+ return ret, nil
+}
+
+func (*DockerTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{model.ContainerImage},
+ OptionalProperties: []string{"container.resources"},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{
+ {Name: model.ContainerImage, IgnoreCase: false, SkipIfMissing: false},
+ {Name: "container.ports", IgnoreCase: false, SkipIfMissing: true},
+ {Name: "container.resources", IgnoreCase: false, SkipIfMissing: true},
+ },
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package helm
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+
+ "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"
+ "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"
+ "github.com/google/uuid"
+ "helm.sh/helm/v3/pkg/action"
+ "helm.sh/helm/v3/pkg/chart"
+ "helm.sh/helm/v3/pkg/chart/loader"
+ "helm.sh/helm/v3/pkg/cli"
+ "helm.sh/helm/v3/pkg/registry"
+ "helm.sh/helm/v3/pkg/release"
+ "k8s.io/cli-runtime/pkg/genericclioptions"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+const (
+ DEFAULT_NAMESPACE = "default"
+ TEMP_CHART_DIR = "/tmp/symphony/charts"
+)
+
+type (
+ // HelmTargetProviderConfig is the configuration for the Helm provider
+ HelmTargetProviderConfig struct {
+ Name string `json:"name"`
+ ConfigType string `json:"configType,omitempty"`
+ ConfigData string `json:"configData,omitempty"`
+ Context string `json:"context,omitempty"`
+ InCluster bool `json:"inCluster"`
+ }
+ // HelmTargetProvider is the Helm provider
+ HelmTargetProvider struct {
+ Config HelmTargetProviderConfig
+ Context *contexts.ManagerContext
+ ListClient *action.List
+ InstallClient *action.Install
+ UpgradeClient *action.Upgrade
+ UninstallClient *action.Uninstall
+ }
+ // HelmProperty is the property for the Helm chart
+ HelmProperty struct {
+ Chart HelmChartProperty `json:"chart"`
+ Values map[string]interface{} `json:"values,omitempty"`
+ }
+ // HelmChartProperty is the property for the Helm Charts
+ HelmChartProperty struct {
+ Repo string `json:"repo"`
+ Version string `json:"version"`
+ Wait bool `json:"wait"`
+ }
+)
+
+// HelmTargetProviderConfigFromMap converts a map to a HelmTargetProviderConfig
+func HelmTargetProviderConfigFromMap(properties map[string]string) (HelmTargetProviderConfig, error) {
+ ret := HelmTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+
+ if v, ok := properties["configType"]; ok {
+ ret.ConfigType = v
+ }
+
+ if v, ok := properties["configData"]; ok {
+ ret.ConfigData = v
+ }
+
+ if v, ok := properties["context"]; ok {
+ ret.Context = v
+ }
+
+ if v, ok := properties["inCluster"]; ok {
+ val := v
+ if val != "" {
+ bVal, err := strconv.ParseBool(val)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid bool value in the 'inCluster' setting of Helm provider", v1alpha2.BadConfig)
+ }
+ ret.InCluster = bVal
+ }
+ }
+
+ return ret, nil
+}
+
+// InitWithMap initializes the HelmTargetProvider with a map
+func (i *HelmTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := HelmTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+
+ return i.Init(config)
+}
+
+func (s *HelmTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+// Init initializes the HelmTargetProvider
+func (i *HelmTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan(
+ "Helm Target Provider",
+ context.TODO(),
+ &map[string]string{
+ "method": "Init",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Info(" P (Helm Target): Init()")
+
+ err = initChartsDir()
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to init charts dir: %+v", err)
+ return err
+ }
+
+ // convert config to HelmTargetProviderConfig type
+ var helmConfig HelmTargetProviderConfig
+ helmConfig, err = toHelmTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): expected HelmTargetProviderConfig: %+v", err)
+ return err
+ }
+
+ i.Config = helmConfig
+ var actionConfig *action.Configuration
+ if i.Config.InCluster {
+ settings := cli.New()
+ actionConfig = new(action.Configuration)
+ // TODO: $HELM_DRIVER set the backend storage driver. Values are: configmap, secret, memory, sql. Do we need to handle this differently?
+ if err = actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), log.Printf); err != nil {
+ sLog.Errorf(" P (Helm Target): failed to init: %+v", err)
+ return err
+ }
+ } else {
+ switch i.Config.ConfigType {
+ case "bytes":
+ if i.Config.ConfigData != "" {
+ var kConfig *rest.Config
+ kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData))
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to init with config bytes: %+v", err)
+ return err
+ }
+
+ namespace := DEFAULT_NAMESPACE
+ actionConfig, err = getActionConfig(context.TODO(), namespace, kConfig)
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to init with config bytes: %+v", err)
+ return err
+ }
+
+ } else {
+ err = v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig)
+ sLog.Errorf(" P (Helm Target): %+v", err)
+ return err
+ }
+ default:
+ err = v1alpha2.NewCOAError(nil, "unrecognized config type, accepted value is: bytes", v1alpha2.BadConfig)
+ sLog.Errorf(" P (Helm Target): %+v", err)
+ return err
+ }
+ }
+
+ i.ListClient = action.NewList(actionConfig)
+ i.InstallClient = action.NewInstall(actionConfig)
+ i.UninstallClient = action.NewUninstall(actionConfig)
+ i.UpgradeClient = action.NewUpgrade(actionConfig)
+ return nil
+}
+
+// getActionConfig returns an action configuration
+func getActionConfig(ctx context.Context, namespace string, config *rest.Config) (*action.Configuration, error) {
+ actionConfig := new(action.Configuration)
+ cliConfig := genericclioptions.NewConfigFlags(false)
+ cliConfig.APIServer = &config.Host
+ cliConfig.BearerToken = &config.BearerToken
+ cliConfig.Namespace = &namespace
+ // Drop their rest.Config and just return inject own
+ wrapper := func(*rest.Config) *rest.Config {
+ return config
+ }
+ cliConfig.WithWrapConfigFn(wrapper)
+ if err := actionConfig.Init(cliConfig, namespace, "secret", log.Printf); err != nil {
+ return nil, err
+ }
+
+ return actionConfig, nil
+}
+
+// toHelmTargetProviderConfig converts a generic IProviderConfig to a HelmTargetProviderConfig
+func toHelmTargetProviderConfig(config providers.IProviderConfig) (HelmTargetProviderConfig, error) {
+ ret := HelmTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+// Get returns the list of components for a given deployment
+func (i *HelmTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ _, span := observability.StartSpan(
+ "Helm Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "Get",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (Helm Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+ i.ListClient.Deployed = true
+ var results []*release.Release
+ results, err = i.ListClient.Run()
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to create Helm list client: %+v", err)
+ return nil, err
+ }
+
+ ret := make([]model.ComponentSpec, 0)
+ for _, component := range references {
+ for _, res := range results {
+ if (deployment.Instance.Scope == "" || res.Namespace == deployment.Instance.Scope) && res.Name == component.Component.Name {
+ repo := ""
+ if strings.HasPrefix(res.Chart.Metadata.Tags, "SYM:") { //we use this special metadata tag to remember the chart URL
+ repo = res.Chart.Metadata.Tags[4:]
+ }
+
+ ret = append(ret, model.ComponentSpec{
+ Name: res.Name,
+ Type: "helm.v3",
+ Properties: map[string]interface{}{
+ "chart": map[string]string{
+ "repo": repo,
+ "version": res.Chart.Metadata.Version,
+ },
+ "values": res.Config,
+ },
+ })
+ }
+ }
+ }
+
+ return ret, nil
+}
+
+// GetValidationRule returns the validation rule for this provider
+func (*HelmTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{"chart"},
+ OptionalProperties: []string{"values"},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{
+ {Name: "chart", IgnoreCase: false, SkipIfMissing: true}, //TODO: deep change detection on interface{}
+ },
+ }
+}
+
+// downloadFile will download a url to a local file. It's efficient because it will
+func downloadFile(url string, fileName string) error {
+ resp, err := http.Get(url)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ fileHandle, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
+ if err != nil {
+ return err
+ }
+ defer fileHandle.Close()
+
+ _, err = io.Copy(fileHandle, resp.Body)
+ return err
+}
+
+// Apply deploys the helm chart for a given deployment
+func (i *HelmTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan(
+ "Helm Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "Apply",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (Helm Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+
+ if isDryRun {
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+
+ for _, component := range step.Components {
+ if component.Action == "update" {
+ var helmProp *HelmProperty
+ helmProp, err = getHelmPropertyFromComponent(component.Component)
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to get Helm properties: %+v", err)
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ return ret, err
+ }
+
+ var fileName string
+ fileName, err = i.pullChart(&helmProp.Chart)
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to pull chart: %+v", err)
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ return ret, err
+ }
+ defer os.Remove(fileName)
+
+ var chart *chart.Chart
+ chart, err = loader.Load(fileName)
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to load chart: %+v", err)
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ return ret, err
+ }
+
+ chart.Metadata.Tags = "SYM:" + helmProp.Chart.Repo //this is not used by Helm SDK, we use this to carry repo info
+ i.configureUpsertClients(component.Component.Name, &helmProp.Chart, &deployment)
+
+ if _, err = i.UpgradeClient.Run(component.Component.Name, chart, helmProp.Values); err != nil {
+ if _, err = i.InstallClient.Run(chart, helmProp.Values); err != nil {
+ sLog.Errorf(" P (Helm Target): failed to apply: %+v", err)
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ return ret, err
+ }
+ }
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.Updated,
+ Message: "",
+ }
+ } else {
+ if component.Component.Type == "helm.v3" {
+ _, err = i.UninstallClient.Run(component.Component.Name)
+ if err != nil {
+ if strings.Contains(err.Error(), "not found") {
+ continue //TODO: better way to detect this error?
+ }
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.DeleteFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P (Helm Target): failed to uninstall Helm chart: %+v", err)
+ return ret, err
+ }
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.Deleted,
+ Message: "",
+ }
+ }
+ }
+ }
+ return ret, nil
+}
+
+func (i *HelmTargetProvider) pullChart(chart *HelmChartProperty) (fileName string, err error) {
+ fileName = fmt.Sprintf("%s/%s.tgz", TEMP_CHART_DIR, uuid.New().String())
+
+ var pullRes *registry.PullResult
+ if strings.HasSuffix(chart.Repo, ".tgz") && strings.HasPrefix(chart.Repo, "http") {
+ err = downloadFile(chart.Repo, fileName)
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to download chart from repo: %+v", err)
+ return "", err
+ }
+ } else {
+ var regClient *registry.Client
+ regClient, err = registry.NewClient()
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to create registry client: %+v", err)
+ return
+ }
+
+ pullRes, err = regClient.Pull(fmt.Sprintf("%s:%s", chart.Repo, chart.Version), registry.PullOptWithChart(true))
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to pull chart from repo: %+v", err)
+ return
+ }
+
+ err = ioutil.WriteFile(fileName, pullRes.Chart.Data, 0644)
+ if err != nil {
+ sLog.Errorf(" P (Helm Target): failed to save chart: %+v", err)
+ return
+ }
+ }
+ return fileName, nil
+}
+
+func (i *HelmTargetProvider) configureUpsertClients(name string, componentProps *HelmChartProperty, deployment *model.DeploymentSpec) {
+ if deployment.Instance.Scope == "" {
+ i.InstallClient.Namespace = DEFAULT_NAMESPACE
+ i.UpgradeClient.Namespace = DEFAULT_NAMESPACE
+ } else {
+ i.InstallClient.Namespace = deployment.Instance.Scope
+ i.UpgradeClient.Namespace = deployment.Instance.Scope
+ }
+
+ i.InstallClient.Wait = componentProps.Wait
+ i.UpgradeClient.Wait = componentProps.Wait
+ i.InstallClient.CreateNamespace = true
+ i.InstallClient.ReleaseName = name
+ i.InstallClient.IsUpgrade = true
+ i.UpgradeClient.Install = true
+ i.UpgradeClient.ResetValues = true
+}
+
+func getHelmPropertyFromComponent(component model.ComponentSpec) (*HelmProperty, error) {
+ ret := HelmProperty{}
+ data, err := json.Marshal(component.Properties)
+ if err != nil {
+ return nil, err
+ }
+
+ err = json.Unmarshal(data, &ret)
+ if err != nil {
+ return nil, err
+ }
+
+ return validateProps(&ret)
+}
+
+func validateProps(props *HelmProperty) (*HelmProperty, error) {
+ if props.Chart.Repo == "" {
+ return nil, errors.New("chart repo is required")
+ }
+
+ return props, nil
+}
+
+func initChartsDir() error {
+ if _, err := os.Stat(TEMP_CHART_DIR); os.IsNotExist(err) {
+ err = os.MkdirAll(TEMP_CHART_DIR, os.ModePerm)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package http
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "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"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type HttpTargetProviderConfig struct {
+ Name string `json:"name"`
+}
+
+type HttpTargetProvider struct {
+ Config HttpTargetProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func HttpTargetProviderConfigFromMap(properties map[string]string) (HttpTargetProviderConfig, error) {
+ ret := HttpTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ return ret, nil
+}
+
+func (i *HttpTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := HttpTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *HttpTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *HttpTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("Http Target Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P(HTTP Target): Init()")
+
+ updateConfig, err := toHttpTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P(HTTP Target): expected HttpTargetProviderConfig: %+v", err)
+ return err
+ }
+ i.Config = updateConfig
+
+ return nil
+}
+func toHttpTargetProviderConfig(config providers.IProviderConfig) (HttpTargetProviderConfig, error) {
+ ret := HttpTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *HttpTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ _, span := observability.StartSpan("Http Target Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof(" P(HTTP Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ // This provider doesn't remember what it does, so it always return nil when asked
+ return nil, nil
+}
+
+func (i *HttpTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("Http Target Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof(" P(HTTP Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ injections := &model.ValueInjections{
+ InstanceId: deployment.Instance.Name,
+ SolutionId: deployment.Instance.Solution,
+ TargetId: deployment.ActiveTarget,
+ }
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+ for _, component := range step.Components {
+ if component.Action == "update" {
+ body := model.ReadPropertyCompat(component.Component.Properties, "http.body", injections)
+ url := model.ReadPropertyCompat(component.Component.Properties, "http.url", injections)
+ method := model.ReadPropertyCompat(component.Component.Properties, "http.method", injections)
+
+ if url == "" {
+ err = errors.New("component doesn't have a http.url property")
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P(HTTP Target): %v", err)
+ return ret, err
+ }
+ if method == "" {
+ method = "POST"
+ }
+ jsonData := []byte(body)
+ var request *http.Request
+ request, err = http.NewRequest(method, url, bytes.NewBuffer(jsonData))
+ if err != nil {
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P(HTTP Target): %v", err)
+ return ret, err
+ }
+ request.Header.Set("Content-Type", "application/json; charset=UTF-8")
+
+ client := &http.Client{}
+ var resp *http.Response
+ resp, err = client.Do(request)
+ if err != nil {
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ sLog.Errorf(" P(HTTP Target): %v", err)
+ return ret, err
+ }
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, err := io.ReadAll(resp.Body)
+ var message string
+ if err != nil {
+ message = err.Error()
+ } else {
+ message = string(bodyBytes)
+ }
+ ret[component.Component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: message,
+ }
+ err = errors.New("HTTP request didn't respond 200 OK")
+ sLog.Errorf(" P(HTTP Target): %v", err)
+ return ret, err
+ }
+ }
+ }
+ return ret, nil
+}
+func (*HttpTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{"http.url"},
+ OptionalProperties: []string{"http.method", "http.body"},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package ingress
+
+import (
+ "context"
+ "encoding/json"
+ "path/filepath"
+ "strconv"
+ "strings"
+
+ "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"
+ "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"
+ corev1 "k8s.io/api/core/v1"
+ networkingv1 "k8s.io/api/networking/v1"
+ kerrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
+ "k8s.io/client-go/discovery"
+ "k8s.io/client-go/discovery/cached/memory"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/restmapper"
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/util/homedir"
+)
+
+var (
+ decUnstructured = yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
+ sLog = logger.NewLogger("coa.runtime")
+)
+
+type (
+ // IngressTargetProviderConfig is the configuration for the ingress target provider
+ IngressTargetProviderConfig struct {
+ Name string `json:"name,omitempty"`
+ ConfigType string `json:"configType,omitempty"`
+ ConfigData string `json:"configData,omitempty"`
+ Context string `json:"context,omitempty"`
+ InCluster bool `json:"inCluster"`
+ }
+
+ // IngressTargetProvider is the kubectl target provider
+ IngressTargetProvider struct {
+ Config IngressTargetProviderConfig
+ Context *contexts.ManagerContext
+ Client kubernetes.Interface
+ DynamicClient dynamic.Interface
+ DiscoveryClient *discovery.DiscoveryClient
+ Mapper *restmapper.DeferredDiscoveryRESTMapper
+ RESTConfig *rest.Config
+ }
+)
+
+// IngressTargetProviderConfigFromMap converts a map to a IngressTargetProviderConfig
+func IngressTargetProviderConfigFromMap(properties map[string]string) (IngressTargetProviderConfig, error) {
+ ret := IngressTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["configType"]; ok {
+ ret.ConfigType = v
+ }
+ if v, ok := properties["configData"]; ok {
+ ret.ConfigData = v
+ }
+ if v, ok := properties["context"]; ok {
+ ret.Context = v
+ }
+ if v, ok := properties["inCluster"]; ok {
+ val := v
+ if val != "" {
+ bVal, err := strconv.ParseBool(val)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid bool value in the 'inCluster' setting of ingress provider", v1alpha2.BadConfig)
+ }
+ ret.InCluster = bVal
+ }
+ }
+ return ret, nil
+}
+
+func (s *IngressTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+// InitWithMap initializes the ingress target provider with a map
+func (i *IngressTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := IngressTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+
+ return i.Init(config)
+}
+
+// Init initializes the ingress target provider
+func (i *IngressTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan(
+ "ConfigMap Target Provider",
+ context.TODO(),
+ &map[string]string{
+ "method": "Init",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Info(" P (ConfigMap Target): Init()")
+
+ updateConfig, err := toIngressTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (ConfigMap Target): expected IngressTargetProviderConfig - %+v", err)
+ return err
+ }
+
+ i.Config = updateConfig
+ var kConfig *rest.Config
+ if i.Config.InCluster {
+ kConfig, err = rest.InClusterConfig()
+ } else {
+ switch i.Config.ConfigType {
+ case "path":
+ if i.Config.ConfigData == "" {
+ if home := homedir.HomeDir(); home != "" {
+ i.Config.ConfigData = filepath.Join(home, ".kube", "config")
+ } else {
+ err = v1alpha2.NewCOAError(nil, "can't locate home direction to read default kubernetes config file, to run in cluster, set inCluster config setting to true", v1alpha2.BadConfig)
+ sLog.Errorf(" P (Ingress Target): %+v", err)
+ return err
+ }
+ }
+ kConfig, err = clientcmd.BuildConfigFromFlags("", i.Config.ConfigData)
+ case "inline":
+ if i.Config.ConfigData != "" {
+ kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData))
+ if err != nil {
+ sLog.Errorf(" P (Ingress Target): %+v", err)
+ return err
+ }
+ } else {
+ err = v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig)
+ sLog.Errorf(" P (Ingress Target): %+v", err)
+ return err
+ }
+ default:
+ err = v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and inline", v1alpha2.BadConfig)
+ sLog.Errorf(" P (Ingress Target): %+v", err)
+ return err
+ }
+ }
+ if err != nil {
+ sLog.Errorf(" P (Ingress Target): %+v", err)
+ return err
+ }
+
+ i.Client, err = kubernetes.NewForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (Ingress Target): %+v", err)
+ return err
+ }
+
+ i.DynamicClient, err = dynamic.NewForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (Ingress Target): %+v", err)
+ return err
+ }
+
+ i.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (Ingress Target): %+v", err)
+ return err
+ }
+
+ i.Mapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(i.DiscoveryClient))
+ i.RESTConfig = kConfig
+ return nil
+}
+
+// toIngressTargetProviderConfig converts a generic IProviderConfig to a IngressTargetProviderConfig
+func toIngressTargetProviderConfig(config providers.IProviderConfig) (IngressTargetProviderConfig, error) {
+ ret := IngressTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+// Get gets the artifacts for a ingress
+func (i *IngressTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ ctx, span := observability.StartSpan(
+ "Ingress Target Provider",
+ ctx, &map[string]string{
+ "method": "Get",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (Ingress Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ ret := make([]model.ComponentSpec, 0)
+ for _, component := range references {
+ var obj *networkingv1.Ingress
+ obj, err = i.Client.NetworkingV1().Ingresses(deployment.Instance.Scope).Get(ctx, component.Component.Name, metav1.GetOptions{})
+ if err != nil {
+ if kerrors.IsNotFound(err) {
+ sLog.Infof(" P (Ingress Target): resource not found: %s", err)
+ continue
+ }
+ sLog.Error(" P (Ingress Target): failed to read object: +%v", err)
+ return nil, err
+ }
+ component.Component.Properties = make(map[string]interface{})
+
+ component.Component.Properties["rules"] = obj.Spec.Rules
+ ret = append(ret, component.Component)
+ }
+
+ return ret, nil
+}
+
+// Apply applies the ingress artifacts
+func (i *IngressTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan(
+ "Ingress Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "Apply",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (Ingress Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+ components = step.GetUpdatedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Type == "ingress" {
+ newIngress := &networkingv1.Ingress{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: component.Name,
+ Namespace: deployment.Instance.Scope,
+ },
+ Spec: networkingv1.IngressSpec{
+ Rules: make([]networkingv1.IngressRule, 0),
+ },
+ }
+ if v, ok := component.Properties["rules"]; ok {
+ jData, _ := json.Marshal(v)
+ var rules []networkingv1.IngressRule
+ err = json.Unmarshal(jData, &rules)
+ if err != nil {
+ sLog.Error(" P (Ingress Target): failed to unmarshal ingress: +%v", err)
+ return ret, err
+ }
+ newIngress.Spec.Rules = rules
+ }
+
+ if v, ok := component.Properties["ingressClassName"]; ok {
+ s, ok := v.(string)
+ if ok {
+ newIngress.Spec.IngressClassName = &s
+ } else {
+ sLog.Error(" P (Ingress Target): failed to convert ingress class name: +%v", v)
+ return ret, err
+ }
+ }
+
+ for k, v := range component.Metadata {
+ if strings.HasPrefix(k, "annotations.") {
+ if newIngress.ObjectMeta.Annotations == nil {
+ newIngress.ObjectMeta.Annotations = make(map[string]string)
+ }
+ newIngress.ObjectMeta.Annotations[k[12:]] = v
+ }
+ }
+
+ i.ensureNamespace(ctx, deployment.Instance.Scope)
+ err = i.applyIngress(ctx, newIngress, deployment.Instance.Scope)
+ if err != nil {
+ sLog.Error(" P (Ingress Target): failed to apply ingress: +%v", err)
+ return ret, err
+ }
+ }
+ }
+ }
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Type == "ingress" {
+ err = i.deleteIngress(ctx, component.Name, deployment.Instance.Scope)
+ if err != nil {
+ sLog.Error(" P (Ingress Target): failed to delete ingress: +%v", err)
+ return ret, err
+ }
+ }
+ }
+ }
+ return ret, nil
+}
+
+// ensureNamespace ensures that the namespace exists
+func (k *IngressTargetProvider) ensureNamespace(ctx context.Context, namespace string) error {
+ _, span := observability.StartSpan(
+ "ConfigMap Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "ensureNamespace",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+
+ _, err = k.Client.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
+ if err == nil {
+ return nil
+ }
+
+ if kerrors.IsNotFound(err) {
+ _, err = k.Client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: namespace,
+ },
+ }, metav1.CreateOptions{})
+ if err != nil {
+ sLog.Error(" P (ConfigMap Target): failed to create namespace: +%v", err)
+ return err
+ }
+
+ } else {
+ sLog.Error(" P (ConfigMap Target): failed to get namespace: +%v", err)
+ return err
+ }
+
+ return nil
+}
+
+// GetValidationRule returns validation rule for the provider
+func (*IngressTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{
+ {
+ Name: "*", //react to all property changes
+ },
+ },
+ ChangeDetectionMetadata: []model.PropertyDesc{
+ {
+ Name: "annotations.*", //react to all annotation changes
+ },
+ },
+ }
+}
+
+// deleteConfigMap deletes a configmap
+func (i *IngressTargetProvider) deleteIngress(ctx context.Context, name string, scope string) error {
+ err := i.Client.NetworkingV1().Ingresses(scope).Delete(ctx, name, metav1.DeleteOptions{})
+ if err != nil {
+ if !kerrors.IsNotFound(err) {
+ sLog.Error(" P (Ingress Target): failed to delete ingress: +%v", err)
+ return err
+ }
+ }
+ return nil
+}
+
+// applyCustomResource applies a custom resource from a byte array
+func (i *IngressTargetProvider) applyIngress(ctx context.Context, ingress *networkingv1.Ingress, scope string) error {
+ existingIngress, err := i.Client.NetworkingV1().Ingresses(scope).Get(ctx, ingress.Name, metav1.GetOptions{})
+ if err != nil {
+ if kerrors.IsNotFound(err) {
+ sLog.Infof(" P (Ingress Target): resource not found: %s", err)
+ _, err = i.Client.NetworkingV1().Ingresses(scope).Create(ctx, ingress, metav1.CreateOptions{})
+ if err != nil {
+ sLog.Error(" P (Ingress Target): failed to create ingress: +%v", err)
+ return err
+ }
+ return nil
+ }
+ sLog.Error(" P (Ingress Target): failed to read object: +%v", err)
+ return err
+ }
+
+ existingIngress.Spec.Rules = ingress.Spec.Rules
+ if ingress.ObjectMeta.Annotations != nil {
+ existingIngress.ObjectMeta.Annotations = ingress.ObjectMeta.Annotations
+ }
+ _, err = i.Client.NetworkingV1().Ingresses(scope).Update(ctx, existingIngress, metav1.UpdateOptions{})
+ if err != nil {
+ sLog.Error(" P (Ingress Target): failed to update ingress: +%v", err)
+ return err
+ }
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package k8s
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/target/k8s/projectors"
+ utils "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/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"
+ "go.opentelemetry.io/otel/trace"
+ v1 "k8s.io/api/apps/v1"
+ apiv1 "k8s.io/api/core/v1"
+ k8s_errors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/util/homedir"
+)
+
+var log = logger.NewLogger("coa.runtime")
+
+const (
+ ENV_NAME string = "SYMPHONY_AGENT_ADDRESS"
+ SINGLE_POD string = "single-pod"
+ SERVICES string = "services"
+ SERVICES_NS string = "ns-services"
+ SERVICES_HNS string = "hns-services" //TODO: future versions
+)
+
+type K8sTargetProviderConfig struct {
+ Name string `json:"name"`
+ ConfigType string `json:"configType,omitempty"`
+ ConfigData string `json:"configData,omitempty"`
+ Context string `json:"context,omitempty"`
+ InCluster bool `json:"inCluster"`
+ Projector string `json:"projector,omitempty"`
+ DeploymentStrategy string `json:"deploymentStrategy,omitempty"`
+ DeleteEmptyNamespace bool `json:"deleteEmptyNamespace"`
+ RetryCount int `json:"retryCount"`
+ RetryIntervalInSec int `json:"retryIntervalInSec"`
+}
+
+type K8sTargetProvider struct {
+ Config K8sTargetProviderConfig
+ Context *contexts.ManagerContext
+ Client kubernetes.Interface
+ DynamicClient dynamic.Interface
+}
+
+func K8sTargetProviderConfigFromMap(properties map[string]string) (K8sTargetProviderConfig, error) {
+ ret := K8sTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["configType"]; ok {
+ ret.ConfigType = v
+ }
+ if ret.ConfigType == "" {
+ ret.ConfigType = "path"
+ }
+ if v, ok := properties["configData"]; ok {
+ ret.ConfigData = v
+ }
+ if v, ok := properties["context"]; ok {
+ ret.Context = v
+ }
+ if v, ok := properties["inCluster"]; ok {
+ val := v
+ if val != "" {
+ bVal, err := strconv.ParseBool(val)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid bool value in the 'inCluster' setting of K8s reference provider", v1alpha2.BadConfig)
+ }
+ ret.InCluster = bVal
+ }
+ }
+ if v, ok := properties["deploymentStrategy"]; ok && v != "" {
+ if v != SERVICES && v != SINGLE_POD && v != SERVICES_NS {
+ return ret, v1alpha2.NewCOAError(nil, fmt.Sprintf("invalid deployment strategy. Expected: %s (default), %s or %s", SINGLE_POD, SERVICES, SERVICES_NS), v1alpha2.BadConfig)
+ }
+ ret.DeploymentStrategy = v
+ } else {
+ ret.DeploymentStrategy = SINGLE_POD
+ }
+ if v, ok := properties["deleteEmptyNamespace"]; ok {
+ val := v
+ if val != "" {
+ bVal, err := strconv.ParseBool(val)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid bool value in the 'deleteEmptyNamespace' setting of K8s reference provider", v1alpha2.BadConfig)
+ }
+ ret.DeleteEmptyNamespace = bVal
+ }
+ }
+ if v, ok := properties["retryCount"]; ok && v != "" {
+ ival, err := strconv.Atoi(v)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid int value in the 'retryCount' setting of K8s reference provider", v1alpha2.BadConfig)
+ }
+ ret.RetryCount = ival
+ } else {
+ ret.RetryCount = 3
+ }
+ if v, ok := properties["retryIntervalInSec"]; ok && v != "" {
+ ival, err := strconv.Atoi(v)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid int value in the 'retryInterval' setting of K8s reference provider", v1alpha2.BadConfig)
+ }
+ ret.RetryIntervalInSec = ival
+ } else {
+ ret.RetryIntervalInSec = 2
+ }
+ return ret, nil
+}
+func (i *K8sTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := K8sTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func (s *K8sTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *K8sTargetProvider) Init(config providers.IProviderConfig) error {
+ updateConfig, err := toK8sTargetProviderConfig(config)
+ if err != nil {
+ return errors.New("expected K8sTargetProviderConfig")
+ }
+ i.Config = updateConfig
+ var kConfig *rest.Config
+ if i.Config.InCluster {
+ kConfig, err = rest.InClusterConfig()
+ } else {
+ switch i.Config.ConfigType {
+ case "path":
+ if i.Config.ConfigData == "" {
+ if home := homedir.HomeDir(); home != "" {
+ i.Config.ConfigData = filepath.Join(home, ".kube", "config")
+ } else {
+ return v1alpha2.NewCOAError(nil, "can't locate home direction to read default kubernetes config file, to run in cluster, set inCluster config setting to true", v1alpha2.BadConfig)
+ }
+ }
+ kConfig, err = clientcmd.BuildConfigFromFlags("", i.Config.ConfigData)
+ case "bytes":
+ if i.Config.ConfigData != "" {
+ kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData))
+ if err != nil {
+ return err
+ }
+ } else {
+ return v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig)
+ }
+ default:
+ return v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and bytes", v1alpha2.BadConfig)
+ }
+ }
+ if err != nil {
+ return err
+ }
+ i.Client, err = kubernetes.NewForConfig(kConfig)
+ if err != nil {
+ return err
+ }
+ i.DynamicClient, err = dynamic.NewForConfig(kConfig)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+func toK8sTargetProviderConfig(config providers.IProviderConfig) (K8sTargetProviderConfig, error) {
+ ret := K8sTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ //ret.Name = providers.LoadEnv(ret.Name)
+ //ret.ConfigPath = providers.LoadEnv(ret.ConfigPath)
+ return ret, err
+}
+
+func (i *K8sTargetProvider) getDeployment(ctx context.Context, scope string, name string) ([]model.ComponentSpec, error) {
+ deployment, err := i.Client.AppsV1().Deployments(scope).Get(ctx, name, metav1.GetOptions{})
+ if err != nil {
+ if k8s_errors.IsNotFound(err) {
+ return nil, nil
+ }
+ return nil, err
+ }
+ components, err := deploymentToComponents(*deployment)
+ if err != nil {
+ log.Infof(" P (K8s Target Provider): getDeployment failed - %s", err.Error())
+ return nil, err
+ }
+ return components, nil
+}
+func (i *K8sTargetProvider) fillServiceMeta(ctx context.Context, scope string, name string, component model.ComponentSpec) error {
+ svc, err := i.Client.CoreV1().Services(scope).Get(ctx, name, metav1.GetOptions{})
+ if err != nil {
+ if k8s_errors.IsNotFound(err) {
+ return nil
+ }
+ return err
+ }
+ if component.Metadata == nil {
+ component.Metadata = make(map[string]string)
+ }
+ portData, _ := json.Marshal(svc.Spec.Ports)
+ component.Metadata["service.ports"] = string(portData)
+ component.Metadata["service.type"] = string(svc.Spec.Type)
+ if svc.ObjectMeta.Name != name {
+ component.Metadata["service.name"] = svc.ObjectMeta.Name
+ }
+ if component.Metadata["service.type"] == "LoadBalancer" {
+ component.Metadata["service.loadBalancerIP"] = svc.Spec.LoadBalancerIP
+ }
+ for k, v := range svc.ObjectMeta.Annotations {
+ component.Metadata["service.annotation."+k] = v
+ }
+ return nil
+}
+func (i *K8sTargetProvider) Get(ctx context.Context, dep model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ ctx, span := observability.StartSpan("K8s Target Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+ log.Infof(" P (K8s Target Provider): getting artifacts: %s - %s", dep.Instance.Scope, dep.Instance.Name)
+
+ var components []model.ComponentSpec
+
+ switch i.Config.DeploymentStrategy {
+ case "", SINGLE_POD:
+ components, err = i.getDeployment(ctx, dep.Instance.Scope, dep.Instance.Name)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to get - %s", err.Error())
+ return nil, err
+ }
+ case SERVICES, SERVICES_NS:
+ components = make([]model.ComponentSpec, 0)
+ scope := dep.Instance.Scope
+ if i.Config.DeploymentStrategy == SERVICES_NS {
+ scope = dep.Instance.Name
+ }
+ slice := dep.GetComponentSlice()
+ for _, component := range slice {
+ var cComponents []model.ComponentSpec
+ cComponents, err = i.getDeployment(ctx, scope, component.Name)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider) - failed to get: %s", err.Error())
+ return nil, err
+ }
+ if len(cComponents) > 1 {
+ err = v1alpha2.NewCOAError(nil, fmt.Sprintf("can't read multiple components when %s strategy or %s strategy is used", SERVICES, SERVICES_NS), v1alpha2.InternalError)
+ return nil, err
+ }
+ if len(cComponents) == 1 {
+ serviceName := cComponents[0].Name
+
+ if cComponents[0].Metadata != nil {
+ if v, ok := cComponents[0].Metadata["service.name"]; ok && v != "" {
+ serviceName = v
+ }
+ }
+ if cComponents[0].Metadata == nil {
+ cComponents[0].Metadata = make(map[string]string)
+ }
+
+ err = i.fillServiceMeta(ctx, scope, serviceName, cComponents[0])
+ if err != nil {
+ log.Debugf("failed to get: %s", err.Error())
+ return nil, err
+ }
+ components = append(components, cComponents...)
+ }
+ }
+ }
+
+ return components, nil
+}
+func (i *K8sTargetProvider) removeService(ctx context.Context, scope string, serviceName string) error {
+ svc, err := i.Client.CoreV1().Services(scope).Get(ctx, serviceName, metav1.GetOptions{})
+ if err == nil && svc != nil {
+ foregroundDeletion := metav1.DeletePropagationForeground
+ err = i.Client.CoreV1().Services(scope).Delete(ctx, serviceName, metav1.DeleteOptions{PropagationPolicy: &foregroundDeletion})
+ if err != nil {
+ if !k8s_errors.IsNotFound(err) {
+ return err
+ }
+ }
+ }
+ return nil
+}
+func (i *K8sTargetProvider) removeDeployment(ctx context.Context, scope string, name string) error {
+ foregroundDeletion := metav1.DeletePropagationForeground
+ err := i.Client.AppsV1().Deployments(scope).Delete(ctx, name, metav1.DeleteOptions{PropagationPolicy: &foregroundDeletion})
+ if err != nil {
+ if !k8s_errors.IsNotFound(err) {
+ return err
+ }
+ }
+
+ return nil
+}
+func (i *K8sTargetProvider) removeNamespace(ctx context.Context, scope string, retryCount int, retryIntervalInSec int) error {
+ _, err := i.Client.CoreV1().Namespaces().Get(ctx, scope, metav1.GetOptions{})
+ if err != nil {
+ return err
+ }
+
+ resourceCount := make(map[string]int)
+ count := 0
+ for {
+ count++
+ podList, _ := i.Client.CoreV1().Pods(scope).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return err
+ }
+
+ if len(podList.Items) == 0 || count == retryCount {
+ resourceCount["pod"] = len(podList.Items)
+ break
+ }
+ time.Sleep(time.Second * time.Duration(retryIntervalInSec))
+ }
+
+ deploymentList, err := i.Client.AppsV1().Deployments(scope).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return err
+ }
+ resourceCount["deployment"] = len(deploymentList.Items)
+
+ serviceList, err := i.Client.CoreV1().Services(scope).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return err
+ }
+ resourceCount["service"] = len(serviceList.Items)
+
+ replicasetList, err := i.Client.AppsV1().ReplicaSets(scope).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return err
+ }
+ resourceCount["replicaset"] = len(replicasetList.Items)
+
+ statefulsetList, err := i.Client.AppsV1().StatefulSets(scope).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return err
+ }
+ resourceCount["statefulset"] = len(statefulsetList.Items)
+
+ daemonsetList, err := i.Client.AppsV1().DaemonSets(scope).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return err
+ }
+ resourceCount["daemonset"] = len(daemonsetList.Items)
+
+ jobList, err := i.Client.BatchV1().Jobs(scope).List(ctx, metav1.ListOptions{})
+ if err != nil {
+ return err
+ }
+ resourceCount["job"] = len(jobList.Items)
+
+ isEmpty := true
+ for resource, count := range resourceCount {
+ if count != 0 {
+ log.Debugf(" P (K8s Target Provider): failed to delete %s namespace as resource %s is not empty", scope, resource)
+ isEmpty = false
+ break
+ }
+ }
+
+ if isEmpty {
+ err = i.Client.CoreV1().Namespaces().Delete(ctx, scope, metav1.DeleteOptions{})
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+func (i *K8sTargetProvider) createNamespace(ctx context.Context, scope string) error {
+ _, err := i.Client.CoreV1().Namespaces().Get(ctx, scope, metav1.GetOptions{})
+ if err != nil {
+ if k8s_errors.IsNotFound(err) {
+ _, err = i.Client.CoreV1().Namespaces().Create(ctx, &apiv1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: scope,
+ },
+ }, metav1.CreateOptions{})
+ if err != nil {
+ return err
+ }
+ } else {
+ return err
+ }
+ }
+ return nil
+}
+func (i *K8sTargetProvider) upsertDeployment(ctx context.Context, scope string, name string, deployment *v1.Deployment) error {
+ existing, err := i.Client.AppsV1().Deployments(scope).Get(ctx, name, metav1.GetOptions{})
+ if err != nil && !k8s_errors.IsNotFound(err) {
+ return err
+ }
+ if k8s_errors.IsNotFound(err) {
+ _, err = i.Client.AppsV1().Deployments(scope).Create(ctx, deployment, metav1.CreateOptions{})
+ } else {
+ deployment.ResourceVersion = existing.ResourceVersion
+ _, err = i.Client.AppsV1().Deployments(scope).Update(ctx, deployment, metav1.UpdateOptions{})
+ }
+ if err != nil {
+ return err
+ }
+ return nil
+}
+func (i *K8sTargetProvider) upsertService(ctx context.Context, scope string, name string, service *apiv1.Service) error {
+ existing, err := i.Client.CoreV1().Services(scope).Get(ctx, name, metav1.GetOptions{})
+ if err != nil && !k8s_errors.IsNotFound(err) {
+ return err
+ }
+ if k8s_errors.IsNotFound(err) {
+ _, err = i.Client.CoreV1().Services(scope).Create(ctx, service, metav1.CreateOptions{})
+ } else {
+ service.ResourceVersion = existing.ResourceVersion
+ _, err = i.Client.CoreV1().Services(scope).Update(ctx, service, metav1.UpdateOptions{})
+ }
+ if err != nil {
+ return err
+ }
+ return nil
+}
+func (i *K8sTargetProvider) deployComponents(ctx context.Context, span trace.Span, scope string, name string, metadata map[string]string, components []model.ComponentSpec, projector IK8sProjector, instanceName string) error {
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ deployment, err := componentsToDeployment(scope, name, metadata, components, instanceName)
+ if projector != nil {
+ err = projector.ProjectDeployment(scope, name, metadata, components, deployment)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to project deployment: %s", err.Error())
+ return err
+ }
+ }
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to apply: %s", err.Error())
+ return err
+ }
+ service, err := metadataToService(scope, name, metadata)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to apply (convert): %s", err.Error())
+ return err
+ }
+ if projector != nil {
+ err = projector.ProjectService(scope, name, metadata, service)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to project service: %s", err.Error())
+ return err
+ }
+ }
+
+ log.Debug(" P (K8s Target Provider): checking namespace")
+ err = i.createNamespace(ctx, scope)
+ if err != nil {
+ log.Debugf("failed to create namespace: %s", err.Error())
+ return err
+ }
+
+ log.Debug(" P (K8s Target Provider): creating deployment")
+ err = i.upsertDeployment(ctx, scope, name, deployment)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to apply (API): %s", err.Error())
+ return err
+ }
+
+ if service != nil {
+ log.Debug(" P (K8s Target Provider): creating service")
+ err = i.upsertService(ctx, scope, service.Name, service)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to apply (service): %s", err.Error())
+ return err
+ }
+ }
+ return nil
+}
+func (*K8sTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{model.ContainerImage},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{
+ {Name: model.ContainerImage, IgnoreCase: true, SkipIfMissing: false},
+ {Name: "env.*", IgnoreCase: true, SkipIfMissing: true},
+ },
+ }
+}
+func (i *K8sTargetProvider) Apply(ctx context.Context, dep model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("K8s Target Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ log.Infof(" P (K8s Target Provider): applying artifacts: %s - %s", dep.Instance.Scope, dep.Instance.Name)
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+
+ projector, err := createProjector(i.Config.Projector)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to create projector: %s", err.Error())
+ return ret, err
+ }
+
+ switch i.Config.DeploymentStrategy {
+ case "", SINGLE_POD:
+ updated := step.GetUpdatedComponents()
+ if len(updated) > 0 {
+ err = i.deployComponents(ctx, span, dep.Instance.Scope, dep.Instance.Name, dep.Instance.Metadata, components, projector, dep.Instance.Name)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to apply components: %s", err.Error())
+ return ret, err
+ }
+ }
+ deleted := step.GetDeletedComponents()
+ if len(deleted) > 0 {
+ serviceName := dep.Instance.Name
+ if v, ok := dep.Instance.Metadata["service.name"]; ok && v != "" {
+ serviceName = v
+ }
+ err = i.removeService(ctx, dep.Instance.Scope, serviceName)
+ if err != nil {
+ log.Debugf("failed to remove service: %s", err.Error())
+ return ret, err
+ }
+ err = i.removeDeployment(ctx, dep.Instance.Scope, dep.Instance.Name)
+ if err != nil {
+ log.Debugf("failed to remove deployment: %s", err.Error())
+ return ret, err
+ }
+ if i.Config.DeleteEmptyNamespace {
+ err = i.removeNamespace(ctx, dep.Instance.Scope, i.Config.RetryCount, i.Config.RetryIntervalInSec)
+ if err != nil {
+ log.Debugf("failed to remove namespace: %s", err.Error())
+ }
+ }
+ }
+ case SERVICES, SERVICES_NS:
+ updated := step.GetUpdatedComponents()
+ if len(updated) > 0 {
+ scope := dep.Instance.Scope
+ if i.Config.DeploymentStrategy == SERVICES_NS {
+ scope = dep.Instance.Name
+ }
+ for _, component := range components {
+ if dep.Instance.Metadata != nil {
+ if v, ok := dep.Instance.Metadata[ENV_NAME]; ok && v != "" {
+ if component.Metadata == nil {
+ component.Metadata = make(map[string]string)
+ }
+ component.Metadata[ENV_NAME] = v
+ }
+ }
+ err = i.deployComponents(ctx, span, scope, component.Name, component.Metadata, []model.ComponentSpec{component}, projector, dep.Instance.Name)
+ if err != nil {
+ log.Debugf(" P (K8s Target Provider): failed to apply components: %s", err.Error())
+ return ret, err
+ }
+ }
+ }
+ deleted := step.GetDeletedComponents()
+ if len(deleted) > 0 {
+ scope := dep.Instance.Scope
+ if i.Config.DeploymentStrategy == SERVICES_NS {
+ scope = dep.Instance.Name
+ }
+ for _, component := range deleted {
+ serviceName := component.Name
+ if component.Metadata != nil {
+ if v, ok := component.Metadata["service.name"]; ok {
+ serviceName = v
+ }
+ }
+ err = i.removeService(ctx, scope, serviceName)
+ if err != nil {
+ ret[component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.DeleteFailed,
+ Message: err.Error(),
+ }
+ log.Debugf("failed to remove service: %s", err.Error())
+ return ret, err
+ }
+ err = i.removeDeployment(ctx, scope, component.Name)
+ if err != nil {
+ ret[component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.DeleteFailed,
+ Message: err.Error(),
+ }
+ log.Debugf("failed to remove deployment: %s", err.Error())
+ return ret, err
+ }
+ if i.Config.DeleteEmptyNamespace {
+ err = i.removeNamespace(ctx, dep.Instance.Scope, i.Config.RetryCount, i.Config.RetryIntervalInSec)
+ if err != nil {
+ log.Debugf("failed to remove namespace: %s", err.Error())
+ }
+ }
+ }
+
+ }
+ }
+ err = nil
+ return ret, nil
+}
+func deploymentToComponents(deployment v1.Deployment) ([]model.ComponentSpec, error) {
+ components := make([]model.ComponentSpec, len(deployment.Spec.Template.Spec.Containers))
+ for i, c := range deployment.Spec.Template.Spec.Containers {
+ component := model.ComponentSpec{
+ Name: c.Name,
+ Properties: make(map[string]interface{}),
+ }
+ component.Properties[model.ContainerImage] = c.Image
+ policy := string(c.ImagePullPolicy)
+ if policy != "" {
+ component.Properties["container.imagePullPolicy"] = policy
+ }
+ if len(c.Ports) > 0 {
+ ports, _ := json.Marshal(c.Ports)
+ component.Properties["container.ports"] = string(ports)
+ }
+ if len(c.Args) > 0 {
+ args, _ := json.Marshal(c.Args)
+ component.Properties["container.args"] = string(args)
+ }
+ if len(c.Command) > 0 {
+ commands, _ := json.Marshal(c.Command)
+ component.Properties["container.commands"] = string(commands)
+ }
+ resources, _ := json.Marshal(c.Resources)
+ if string(resources) != "{}" {
+ component.Properties["container.resources"] = string(resources)
+ }
+ if len(c.VolumeMounts) > 0 {
+ volumeMounts, _ := json.Marshal(c.VolumeMounts)
+ component.Properties["container.volumeMounts"] = string(volumeMounts)
+ }
+ if len(c.Env) > 0 {
+ for _, e := range c.Env {
+ component.Properties["env."+e.Name] = e.Value
+ }
+ }
+ components[i] = component
+ }
+ return components, nil
+}
+func metadataToService(scope string, name string, metadata map[string]string) (*apiv1.Service, error) {
+ if len(metadata) == 0 {
+ return nil, nil
+ }
+
+ servicePorts := make([]apiv1.ServicePort, 0)
+
+ if v, ok := metadata["service.ports"]; ok && v != "" {
+ log.Debugf(" P (K8s Target Provider): metadataToService - service ports: %s", v)
+ e := json.Unmarshal([]byte(v), &servicePorts)
+ if e != nil {
+ log.Errorf(" P (K8s Target Provider): metadataToService - unmarshal: %v", e)
+ return nil, e
+ }
+ } else {
+ return nil, nil
+ }
+
+ serviceName := utils.ReadString(metadata, "service.name", name)
+ serviceType := utils.ReadString(metadata, "service.type", "ClusterIP")
+
+ service := apiv1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: serviceName,
+ Namespace: scope,
+ Labels: map[string]string{
+ "app": name,
+ },
+ },
+ Spec: apiv1.ServiceSpec{
+ Type: apiv1.ServiceType(serviceType),
+ Ports: servicePorts,
+ Selector: map[string]string{
+ "app": name,
+ },
+ },
+ }
+ if _, ok := metadata["service.loadBalancerIP"]; ok {
+ service.Spec.LoadBalancerIP = utils.ReadString(metadata, "service.loadBalancerIP", "")
+ }
+ annotations := utils.CollectStringMap(metadata, "service.annotation.")
+ if len(annotations) > 0 {
+ service.ObjectMeta.Annotations = make(map[string]string)
+ for k, v := range annotations {
+ service.ObjectMeta.Annotations[k[19:]] = v
+ }
+ }
+ return &service, nil
+}
+func int32Ptr(i int32) *int32 { return &i }
+func componentsToDeployment(scope string, name string, metadata map[string]string, components []model.ComponentSpec, instanceName string) (*v1.Deployment, error) {
+ deployment := v1.Deployment{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ },
+ Spec: v1.DeploymentSpec{
+ Replicas: int32Ptr(utils.ReadInt32(metadata, "deployment.replicas", 1)),
+ Selector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{
+ "app": name,
+ },
+ },
+ Template: apiv1.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{
+ Labels: map[string]string{
+ "app": name,
+ },
+ },
+ Spec: apiv1.PodSpec{
+ Containers: []apiv1.Container{},
+ },
+ },
+ },
+ }
+
+ for _, c := range components {
+ ports := make([]apiv1.ContainerPort, 0)
+ if v, ok := c.Properties["container.ports"].(string); ok && v != "" {
+ e := json.Unmarshal([]byte(v), &ports)
+ if e != nil {
+ return nil, e
+ }
+ }
+ container := apiv1.Container{
+ Name: c.Name,
+ Image: c.Properties[model.ContainerImage].(string),
+ Ports: ports,
+ ImagePullPolicy: apiv1.PullPolicy(utils.ReadStringFromMapCompat(c.Properties, "container.imagePullPolicy", "Always")),
+ }
+ if v, ok := c.Properties["container.args"]; ok && v != "" {
+ args := make([]string, 0)
+ e := json.Unmarshal([]byte(fmt.Sprintf("%v", v)), &args)
+ if e != nil {
+ return nil, e
+ }
+ container.Args = args
+ }
+ if v, ok := c.Properties["container.commands"]; ok && v != "" {
+ cmds := make([]string, 0)
+ e := json.Unmarshal([]byte(fmt.Sprintf("%v", v)), &cmds)
+ if e != nil {
+ return nil, e
+ }
+ container.Command = cmds
+ }
+ if v, ok := c.Properties["container.resources"]; ok && v != "" {
+ res := apiv1.ResourceRequirements{}
+ e := json.Unmarshal([]byte(fmt.Sprintf("%v", v)), &res)
+ if e != nil {
+ return nil, e
+ }
+ container.Resources = res
+ }
+ if v, ok := c.Properties["container.volumeMounts"]; ok && v != "" {
+ mounts := make([]apiv1.VolumeMount, 0)
+ e := json.Unmarshal([]byte(fmt.Sprintf("%v", v)), &mounts)
+ if e != nil {
+ return nil, e
+ }
+ container.VolumeMounts = mounts
+ }
+ for k, v := range c.Properties {
+ // Transitioning from map[string]string to map[string]interface{}
+ // for now we'll assume that all relevant values are strings till we
+ // refactor the code to handle the new format
+ sv := fmt.Sprintf("%v", v)
+ if strings.HasPrefix(k, "env.") {
+ if container.Env == nil {
+ container.Env = make([]apiv1.EnvVar, 0)
+ }
+ container.Env = append(container.Env, apiv1.EnvVar{
+ Name: k[4:],
+ Value: sv,
+ })
+ }
+ }
+ agentName := metadata[ENV_NAME]
+ if agentName != "" {
+ if container.Env == nil {
+ container.Env = make([]apiv1.EnvVar, 0)
+ }
+ container.Env = append(container.Env, apiv1.EnvVar{
+ Name: ENV_NAME,
+ Value: agentName + ".default.svc.cluster.local", //agent is currently always installed under deault
+ })
+ }
+ deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, container)
+ }
+ if v, ok := metadata["deployment.imagePullSecrets"]; ok && v != "" {
+ secrets := make([]apiv1.LocalObjectReference, 0)
+ e := json.Unmarshal([]byte(v), &secrets)
+ if e != nil {
+ return nil, e
+ }
+ deployment.Spec.Template.Spec.ImagePullSecrets = secrets
+ }
+ if v, ok := metadata["pod.volumes"]; ok && v != "" {
+ volumes := make([]apiv1.Volume, 0)
+ e := json.Unmarshal([]byte(v), &volumes)
+ if e != nil {
+ return nil, e
+ }
+ deployment.Spec.Template.Spec.Volumes = volumes
+ }
+ if v, ok := metadata["deployment.nodeSelector"]; ok && v != "" {
+ selector := make(map[string]string)
+ e := json.Unmarshal([]byte(v), &selector)
+ if e != nil {
+ return nil, e
+ }
+ deployment.Spec.Template.Spec.NodeSelector = selector
+ }
+
+ data, _ := json.Marshal(deployment)
+ log.Debug(string(data))
+
+ return &deployment, nil
+}
+
+func createProjector(projector string) (IK8sProjector, error) {
+ switch projector {
+ case "noop":
+ return &projectors.NoOpProjector{}, nil
+ case "":
+ return nil, nil
+ }
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("project type '%s' is unsupported", projector), v1alpha2.BadConfig)
+}
+
+type IK8sProjector interface {
+ ProjectDeployment(scope string, name string, metadata map[string]string, components []model.ComponentSpec, deployment *v1.Deployment) error
+ ProjectService(scope string, name string, metadata map[string]string, service *apiv1.Service) error
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package projectors
+
+import (
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+ v1 "k8s.io/api/apps/v1"
+ apiv1 "k8s.io/api/core/v1"
+)
+
+type NoOpProjector struct {
+}
+
+func (p *NoOpProjector) ProjectDeployment(scope string, name string, metadata map[string]string, components []model.ComponentSpec, deployment *v1.Deployment) error {
+ return nil
+}
+func (p *NoOpProjector) ProjectService(scope string, name string, metadata map[string]string, service *apiv1.Service) error {
+ if name == "error" {
+ return v1alpha2.NewCOAError(nil, "throw error project service", v1alpha2.BadConfig)
+ }
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package kubectl
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "net/http"
+ "path/filepath"
+ "strconv"
+
+ "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"
+ "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"
+ corev1 "k8s.io/api/core/v1"
+ kerrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
+ utilyaml "k8s.io/apimachinery/pkg/util/yaml"
+ "k8s.io/client-go/discovery"
+ "k8s.io/client-go/discovery/cached/memory"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/restmapper"
+ "k8s.io/client-go/tools/clientcmd"
+ "k8s.io/client-go/util/homedir"
+)
+
+var (
+ decUnstructured = yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
+ sLog = logger.NewLogger("coa.runtime")
+)
+
+type (
+ // KubectlTargetProviderConfig is the configuration for the kubectl target provider
+ KubectlTargetProviderConfig struct {
+ Name string `json:"name,omitempty"`
+ ConfigType string `json:"configType,omitempty"`
+ ConfigData string `json:"configData,omitempty"`
+ Context string `json:"context,omitempty"`
+ InCluster bool `json:"inCluster"`
+ }
+
+ // KubectlTargetProvider is the kubectl target provider
+ KubectlTargetProvider struct {
+ Config KubectlTargetProviderConfig
+ Context *contexts.ManagerContext
+ Client kubernetes.Interface
+ DynamicClient dynamic.Interface
+ DiscoveryClient *discovery.DiscoveryClient
+ Mapper *restmapper.DeferredDiscoveryRESTMapper
+ RESTConfig *rest.Config
+ }
+)
+
+// KubectlTargetProviderConfigFromMap converts a map to a KubectlTargetProviderConfig
+func KubectlTargetProviderConfigFromMap(properties map[string]string) (KubectlTargetProviderConfig, error) {
+ ret := KubectlTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["configType"]; ok {
+ ret.ConfigType = v
+ }
+ if v, ok := properties["configData"]; ok {
+ ret.ConfigData = v
+ }
+ if v, ok := properties["context"]; ok {
+ ret.Context = v
+ }
+ if v, ok := properties["inCluster"]; ok {
+ val := v
+ if val != "" {
+ bVal, err := strconv.ParseBool(val)
+ if err != nil {
+ return ret, v1alpha2.NewCOAError(err, "invalid bool value in the 'inCluster' setting of kubectl provider", v1alpha2.BadConfig)
+ }
+ ret.InCluster = bVal
+ }
+ }
+ return ret, nil
+}
+
+// InitWithMap initializes the kubectl target provider with a map
+func (i *KubectlTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := KubectlTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+
+ return i.Init(config)
+}
+
+func (s *KubectlTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+// Init initializes the kubectl target provider
+func (i *KubectlTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan(
+ "Kubectl Target Provider",
+ context.TODO(),
+ &map[string]string{
+ "method": "Init",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Info(" P (Kubectl Target): Init()")
+
+ updateConfig, err := toKubectlTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (Kubectl Target): expected KubectlTargetProviderConfig - %+v", err)
+ return err
+ }
+
+ i.Config = updateConfig
+ var kConfig *rest.Config
+ if i.Config.InCluster {
+ kConfig, err = rest.InClusterConfig()
+ } else {
+ switch i.Config.ConfigType {
+ case "path":
+ if i.Config.ConfigData == "" {
+ if home := homedir.HomeDir(); home != "" {
+ i.Config.ConfigData = filepath.Join(home, ".kube", "config")
+ } else {
+ err = v1alpha2.NewCOAError(nil, "can't locate home direction to read default kubernetes config file, to run in cluster, set inCluster config setting to true", v1alpha2.BadConfig)
+ sLog.Errorf(" P (Kubectl Target): %+v", err)
+ return err
+ }
+ }
+ kConfig, err = clientcmd.BuildConfigFromFlags("", i.Config.ConfigData)
+ case "inline":
+ if i.Config.ConfigData != "" {
+ kConfig, err = clientcmd.RESTConfigFromKubeConfig([]byte(i.Config.ConfigData))
+ if err != nil {
+ sLog.Errorf(" P (Kubectl Target): %+v", err)
+ return err
+ }
+ } else {
+ err = v1alpha2.NewCOAError(nil, "config data is not supplied", v1alpha2.BadConfig)
+ sLog.Errorf(" P (Kubectl Target): %+v", err)
+ return err
+ }
+ default:
+ err = v1alpha2.NewCOAError(nil, "unrecognized config type, accepted values are: path and inline", v1alpha2.BadConfig)
+ sLog.Errorf(" P (Kubectl Target): %+v", err)
+ return err
+ }
+ }
+ if err != nil {
+ sLog.Errorf(" P (Kubectl Target): %+v", err)
+ return err
+ }
+
+ i.Client, err = kubernetes.NewForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (Kubectl Target): %+v", err)
+ return err
+ }
+
+ i.DynamicClient, err = dynamic.NewForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (Kubectl Target): %+v", err)
+ return err
+ }
+
+ i.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(kConfig)
+ if err != nil {
+ sLog.Errorf(" P (Kubectl Target): %+v", err)
+ return err
+ }
+
+ i.Mapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(i.DiscoveryClient))
+ i.RESTConfig = kConfig
+ return nil
+}
+
+// toKubectlTargetProviderConfig converts a generic IProviderConfig to a KubectlTargetProviderConfig
+func toKubectlTargetProviderConfig(config providers.IProviderConfig) (KubectlTargetProviderConfig, error) {
+ ret := KubectlTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+// Get gets the artifacts for a deployment
+func (i *KubectlTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ ctx, span := observability.StartSpan(
+ "Kubectl Target Provider",
+ ctx, &map[string]string{
+ "method": "Get",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (Kubectl Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ ret := make([]model.ComponentSpec, 0)
+ for _, component := range references {
+ if v, ok := component.Component.Properties["yaml"].(string); ok {
+ chanMes, chanErr := readYaml(v)
+ stop := false
+ for !stop {
+ select {
+ case dataBytes, ok := <-chanMes:
+ if !ok {
+ err = errors.New("failed to receive from data channel")
+ sLog.Error(" P (Kubectl Target): +%v", err)
+ return nil, err
+ }
+
+ _, err = i.getCustomResource(ctx, dataBytes, deployment.Instance.Scope)
+ if err != nil {
+ if kerrors.IsNotFound(err) {
+ sLog.Infof(" P (Kubectl Target): resource not found: %s", err)
+ continue
+ }
+ sLog.Error(" P (Kubectl Target): failed to read object: +%v", err)
+ return nil, err
+ }
+
+ ret = append(ret, component.Component)
+ stop = true //we do early stop as soon as we found the first resource. we may want to support different strategy in the future
+
+ case err, ok := <-chanErr:
+ if !ok {
+ err = errors.New("failed to receive from error channel")
+ sLog.Error(" P (Kubectl Target): +%v", err)
+ return nil, err
+ }
+
+ if err == io.EOF {
+ stop = true
+ } else {
+ sLog.Error(" P (Kubectl Target): failed to apply Yaml: +%v", err)
+ return nil, err
+ }
+ }
+ }
+ } else if component.Component.Properties["resource"] != nil {
+ var dataBytes []byte
+ dataBytes, err = json.Marshal(component.Component.Properties["resource"])
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to get deployment bytes from component: +%v", err)
+ return nil, err
+ }
+
+ _, err = i.getCustomResource(ctx, dataBytes, deployment.Instance.Scope)
+ if err != nil {
+ if kerrors.IsNotFound(err) {
+ sLog.Infof(" P (Kubectl Target): resource not found: %s", err)
+ continue
+ }
+ sLog.Error(" P (Kubectl Target): failed to read object: +%v", err)
+ return nil, err
+ }
+
+ ret = append(ret, component.Component)
+
+ } else {
+ err = errors.New("component doesn't have yaml or resource property")
+ sLog.Error(" P (Kubectl Target): component doesn't have yaml or resource property")
+ return nil, err
+ }
+ }
+
+ return ret, nil
+}
+
+// Apply applies the deployment artifacts
+func (i *KubectlTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan(
+ "Kubectl Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "Apply",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (Kubectl Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+ components = step.GetUpdatedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Type == "yaml.k8s" {
+ if v, ok := component.Properties["yaml"].(string); ok {
+ chanMes, chanErr := readYaml(v)
+ stop := false
+ for !stop {
+ select {
+ case dataBytes, ok := <-chanMes:
+ if !ok {
+ err = errors.New("failed to receive from data channel")
+ sLog.Error(" P (Kubectl Target): +%v", err)
+ return ret, err
+ }
+
+ i.ensureNamespace(ctx, deployment.Instance.Scope)
+ err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Scope)
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to apply Yaml: +%v", err)
+ return ret, err
+ }
+
+ case err, ok := <-chanErr:
+ if !ok {
+ err = errors.New("failed to receive from error channel")
+ sLog.Error(" P (Kubectl Target): +%v", err)
+ return ret, err
+ }
+
+ if err == io.EOF {
+ stop = true
+ } else {
+ sLog.Error(" P (Kubectl Target): failed to apply Yaml: +%v", err)
+ return ret, err
+ }
+ }
+ }
+ } else if component.Properties["resource"] != nil {
+ var dataBytes []byte
+ dataBytes, err = json.Marshal(component.Properties["resource"])
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to convert resource data to bytes: +%v", err)
+ return ret, err
+ }
+
+ i.ensureNamespace(ctx, deployment.Instance.Scope)
+ err = i.applyCustomResource(ctx, dataBytes, deployment.Instance.Scope)
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to apply custom resource: +%v", err)
+ return ret, err
+ }
+
+ } else {
+ err = errors.New("component doesn't have yaml property or resource property")
+ sLog.Error(" P (Kubectl Target): component doesn't have yaml property or resource property")
+ return ret, err
+ }
+ }
+ }
+ }
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Type == "yaml.k8s" {
+ if v, ok := component.Properties["yaml"].(string); ok {
+ chanMes, chanErr := readYaml(v)
+ stop := false
+ for !stop {
+ select {
+ case dataBytes, ok := <-chanMes:
+ if !ok {
+ err = errors.New("failed to receive from data channel")
+ sLog.Error(" P (Kubectl Target): +%v", err)
+ return ret, err
+ }
+
+ err = i.deleteCustomResource(ctx, dataBytes, deployment.Instance.Scope)
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to read object: +%v", err)
+ return ret, err
+ }
+
+ case err, ok := <-chanErr:
+ if !ok {
+ err = errors.New("failed to receive from error channel")
+ sLog.Error(" P (Kubectl Target): +%v", err)
+ return ret, err
+ }
+
+ if err == io.EOF {
+ stop = true
+ } else {
+ sLog.Error(" P (Kubectl Target): failed to remove resource: +%v", err)
+ return ret, err
+ }
+ }
+ }
+ } else if component.Properties["resource"] != nil {
+ var dataBytes []byte
+ dataBytes, err = json.Marshal(component.Properties["resource"])
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to convert resource data to bytes: +%v", err)
+ return ret, err
+ }
+
+ err = i.deleteCustomResource(ctx, dataBytes, deployment.Instance.Scope)
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to delete custom resource: +%v", err)
+ return ret, err
+ }
+
+ } else {
+ err = errors.New("component doesn't have yaml property or resource property")
+ sLog.Error(" P (Kubectl Target): component doesn't have yaml property or resource property")
+ return ret, err
+ }
+ }
+ }
+ }
+ return ret, nil
+}
+
+// ensureNamespace ensures that the namespace exists
+func (k *KubectlTargetProvider) ensureNamespace(ctx context.Context, namespace string) error {
+ _, span := observability.StartSpan(
+ "Kubectl Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "ensureNamespace",
+ },
+ )
+ var err error
+ defer utils.CloseSpanWithError(span, &err)
+
+ _, err = k.Client.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
+ if err == nil {
+ return nil
+ }
+
+ if kerrors.IsNotFound(err) {
+ _, err = k.Client.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: namespace,
+ },
+ }, metav1.CreateOptions{})
+ if err != nil {
+ sLog.Error("~~~ Kubectl Target Provider ~~~ : failed to create namespace: +%v", err)
+ return err
+ }
+
+ } else {
+ sLog.Error("~~~ Kubectl Target Provider ~~~ : failed to get namespace: +%v", err)
+ return err
+ }
+
+ return nil
+}
+
+// GetValidationRule returns validation rule for the provider
+func (*KubectlTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{"yaml", "resource"},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{
+ {
+ Name: "*", //react to all property changes
+ },
+ },
+ }
+}
+
+// ReadYaml reads yaml from url
+func readYaml(yaml string) (<-chan []byte, <-chan error) {
+ var (
+ chanErr = make(chan error)
+ chanBytes = make(chan []byte)
+ )
+ go func() {
+ response, err := http.Get(yaml)
+ if err != nil {
+ chanErr <- err
+ return
+ }
+ defer response.Body.Close()
+
+ data, err := io.ReadAll(response.Body)
+ if err != nil {
+ chanErr <- err
+ return
+ }
+
+ multidocReader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data)))
+ for {
+ buf, err := multidocReader.Read()
+ if err != nil {
+ chanErr <- err
+ return
+ }
+
+ chanBytes <- buf
+ }
+
+ }()
+ return chanBytes, chanErr
+}
+
+// BuildDynamicResourceClient builds a new dynamic client
+func (i KubectlTargetProvider) buildDynamicResourceClient(data []byte, scope string) (obj *unstructured.Unstructured, dr dynamic.ResourceInterface, err error) {
+ // Decode YAML manifest into unstructured.Unstructured
+ obj = &unstructured.Unstructured{}
+ _, gvk, err := decUnstructured.Decode(data, nil, obj)
+ if err != nil {
+ return obj, dr, err
+ }
+
+ // Find GVR
+ mapping, err := i.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
+ if err != nil {
+ return obj, dr, err
+ }
+
+ i.DynamicClient, err = dynamic.NewForConfig(i.RESTConfig)
+ if err != nil {
+ return obj, dr, err
+ }
+
+ // Obtain REST interface for the GVR
+ if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
+ // namespaced resources should specify the namespace
+ obj.SetNamespace(scope)
+ dr = i.DynamicClient.Resource(mapping.Resource).Namespace(scope)
+ } else {
+ // for cluster-wide resources
+ dr = i.DynamicClient.Resource(mapping.Resource)
+ }
+
+ return obj, dr, nil
+}
+
+// getCustomResource gets a custom resource from a byte array
+func (i *KubectlTargetProvider) getCustomResource(ctx context.Context, dataBytes []byte, scope string) (*unstructured.Unstructured, error) {
+ obj, dr, err := i.buildDynamicResourceClient(dataBytes, scope)
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to build a new dynamic client: +%v", err)
+ return nil, err
+ }
+
+ obj, err = dr.Get(ctx, obj.GetName(), metav1.GetOptions{})
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to read object: +%v", err)
+ return nil, err
+ }
+
+ return obj, nil
+}
+
+// deleteCustomResource deletes a custom resource from a byte array
+func (i *KubectlTargetProvider) deleteCustomResource(ctx context.Context, dataBytes []byte, scope string) error {
+ obj, dr, err := i.buildDynamicResourceClient(dataBytes, scope)
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to build a new dynamic client: +%v", err)
+ return err
+ }
+
+ err = dr.Delete(ctx, obj.GetName(), metav1.DeleteOptions{})
+ if err != nil {
+ if !kerrors.IsNotFound(err) {
+ sLog.Error(" P (Kubectl Target): failed to delete Yaml: +%v", err)
+ return err
+ }
+ }
+
+ return nil
+}
+
+// applyCustomResource applies a custom resource from a byte array
+func (i *KubectlTargetProvider) applyCustomResource(ctx context.Context, dataBytes []byte, scope string) error {
+ obj, dr, err := i.buildDynamicResourceClient(dataBytes, scope)
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to build a new dynamic client: +%v", err)
+ return err
+ }
+
+ // Check if the object exists
+ existing, err := dr.Get(ctx, obj.GetName(), metav1.GetOptions{})
+ if err != nil {
+ if !kerrors.IsNotFound(err) {
+ sLog.Error(" P (Kubectl Target): failed to read object: +%v", err)
+ return err
+ }
+ // Create the object
+ _, err = dr.Create(ctx, obj, metav1.CreateOptions{})
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to create Yaml: +%v", err)
+ return err
+ }
+ return nil
+ }
+
+ // Update the object
+ obj.SetResourceVersion(existing.GetResourceVersion())
+ _, err = dr.Update(ctx, obj, metav1.UpdateOptions{})
+ if err != nil {
+ sLog.Error(" P (Kubectl Target): failed to apply Yaml: +%v", err)
+ return err
+ }
+
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package mock
+
+import (
+ "context"
+ "encoding/json"
+ "sync"
+
+ "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"
+)
+
+type MockTargetProviderConfig struct {
+ ID string `json:"id"`
+}
+type MockTargetProvider struct {
+ Config MockTargetProviderConfig
+ Context *contexts.ManagerContext
+}
+
+var cache map[string][]model.ComponentSpec
+var mLock sync.Mutex
+
+func (m *MockTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan(
+ "Mock Target Provider",
+ context.TODO(),
+ &map[string]string{
+ "method": "Init",
+ },
+ )
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ mLock.Lock()
+ defer mLock.Unlock()
+
+ mockConfig, err := toMockTargetProviderConfig(config)
+ if err != nil {
+ return err
+ }
+ m.Config = mockConfig
+ if cache == nil {
+ cache = make(map[string][]model.ComponentSpec)
+ }
+ return nil
+}
+func (s *MockTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func toMockTargetProviderConfig(config providers.IProviderConfig) (MockTargetProviderConfig, error) {
+ ret := MockTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *MockTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := MockTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+func MockTargetProviderConfigFromMap(properties map[string]string) (MockTargetProviderConfig, error) {
+ ret := MockTargetProviderConfig{}
+ ret.ID = properties["id"]
+ return ret, nil
+}
+func (m *MockTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ mLock.Lock()
+ defer mLock.Unlock()
+
+ _, span := observability.StartSpan(
+ "Mock Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "Get",
+ },
+ )
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+ ret := make([]model.ComponentSpec, 0)
+ for _, c := range cache[m.Config.ID] {
+ for _, r := range references {
+ if c.Name == r.Component.Name {
+ ret = append(ret, c)
+ break
+ }
+ }
+ }
+ return ret, nil
+}
+func (m *MockTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ _, span := observability.StartSpan(
+ "Mock Target Provider",
+ ctx,
+ &map[string]string{
+ "method": "Apply",
+ },
+ )
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ mLock.Lock()
+ defer mLock.Unlock()
+ if cache[m.Config.ID] == nil {
+ cache[m.Config.ID] = make([]model.ComponentSpec, 0)
+ }
+ for _, c := range step.Components {
+ found := false
+ for i, _ := range cache[m.Config.ID] {
+ if cache[m.Config.ID][i].Name == c.Component.Name {
+ found = true
+ if c.Action == "delete" {
+ cache[m.Config.ID] = append(cache[m.Config.ID][:i], cache[m.Config.ID][i+1:]...)
+ }
+ break
+ }
+ }
+ if !found {
+ cache[m.Config.ID] = append(cache[m.Config.ID], c.Component)
+ }
+ }
+ ret := make(map[string]model.ComponentResultSpec)
+ for _, c := range cache[m.Config.ID] {
+ ret[c.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.OK,
+ Message: "",
+ }
+ }
+ return ret, nil
+}
+func (m *MockTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{}
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package mqtt
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "sync"
+ "time"
+
+ "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"
+ gmqtt "github.com/eclipse/paho.mqtt.golang"
+ "github.com/google/uuid"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type MQTTTargetProviderConfig struct {
+ Name string `json:"name"`
+ BrokerAddress string `json:"brokerAddress"`
+ ClientID string `json:"clientID"`
+ RequestTopic string `json:"requestTopic"`
+ ResponseTopic string `json:"responseTopic"`
+ TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
+ KeepAliveSeconds int `json:"keepAliveSeconds,omitempty"`
+ PingTimeoutSeconds int `json:"pingTimeoutSeconds,omitempty"`
+}
+
+var lock sync.Mutex
+
+type ProxyResponse struct {
+ IsOK bool
+ State v1alpha2.State
+ Payload interface{}
+}
+type MQTTTargetProvider struct {
+ Config MQTTTargetProviderConfig
+ Context *contexts.ManagerContext
+ MQTTClient gmqtt.Client
+ GetChan chan ProxyResponse
+ RemoveChan chan ProxyResponse
+ NeedsUpdateChan chan ProxyResponse
+ NeedsRemoveChan chan ProxyResponse
+ ApplyChan chan ProxyResponse
+ Initialized bool
+}
+
+func MQTTTargetProviderConfigFromMap(properties map[string]string) (MQTTTargetProviderConfig, error) {
+ ret := MQTTTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["brokerAddress"]; ok {
+ ret.BrokerAddress = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "'brokerAdress' is missing in MQTT provider config", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["clientID"]; ok {
+ ret.ClientID = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "'clientID' is missing in MQTT provider config", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["requestTopic"]; ok {
+ ret.RequestTopic = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "'requestTopic' is missing in MQTT provider config", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["responseTopic"]; ok {
+ ret.ResponseTopic = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "'responseTopic' is missing in MQTT provider config", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["timeoutSeconds"]; ok {
+ if num, err := strconv.Atoi(v); err == nil {
+ ret.TimeoutSeconds = num
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "'timeoutSeconds' is not an integer in MQTT provider config", v1alpha2.BadConfig)
+ }
+ } else {
+ ret.TimeoutSeconds = 8
+ }
+ if v, ok := properties["keepAliveSeconds"]; ok {
+ if num, err := strconv.Atoi(v); err == nil {
+ ret.KeepAliveSeconds = num
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "'keepAliveSeconds' is not an integer in MQTT provider config", v1alpha2.BadConfig)
+ }
+ } else {
+ ret.KeepAliveSeconds = 2
+ }
+ if v, ok := properties["pingTimeoutSeconds"]; ok {
+ if num, err := strconv.Atoi(v); err == nil {
+ ret.PingTimeoutSeconds = num
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "'pingTimeoutSeconds' is not an integer in MQTT provider config", v1alpha2.BadConfig)
+ }
+ } else {
+ ret.PingTimeoutSeconds = 1
+ }
+ return ret, nil
+}
+
+func (i *MQTTTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := MQTTTargetProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *MQTTTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *MQTTTargetProvider) Init(config providers.IProviderConfig) error {
+ lock.Lock()
+ defer lock.Unlock()
+
+ _, span := observability.StartSpan("MQTT Target Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (MQTT Target): Init()")
+
+ if i.Initialized {
+ return nil
+ }
+ updateConfig, err := toMQTTTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (MQTT Target): expected HttpTargetProviderConfig: %+v", err)
+ return err
+ }
+ i.Config = updateConfig
+ id := uuid.New()
+ opts := gmqtt.NewClientOptions().AddBroker(i.Config.BrokerAddress).SetClientID(id.String())
+ opts.SetKeepAlive(time.Duration(i.Config.KeepAliveSeconds) * time.Second)
+ opts.SetPingTimeout(time.Duration(i.Config.PingTimeoutSeconds) * time.Second)
+ opts.CleanSession = true
+ i.MQTTClient = gmqtt.NewClient(opts)
+ if token := i.MQTTClient.Connect(); token.Wait() && token.Error() != nil {
+ sLog.Errorf(" P (MQTT Target): faild to connect to MQTT broker - %+v", err)
+ return v1alpha2.NewCOAError(token.Error(), "failed to connect to MQTT broker", v1alpha2.InternalError)
+ }
+
+ i.GetChan = make(chan ProxyResponse)
+ i.RemoveChan = make(chan ProxyResponse)
+ i.NeedsUpdateChan = make(chan ProxyResponse)
+ i.NeedsRemoveChan = make(chan ProxyResponse)
+ i.ApplyChan = make(chan ProxyResponse)
+
+ if token := i.MQTTClient.Subscribe(i.Config.ResponseTopic, 0, func(client gmqtt.Client, msg gmqtt.Message) {
+ var response v1alpha2.COAResponse
+ json.Unmarshal(msg.Payload(), &response)
+ proxyResponse := ProxyResponse{
+ IsOK: response.State == v1alpha2.OK || response.State == v1alpha2.Accepted,
+ State: response.State,
+ }
+ if !proxyResponse.IsOK {
+ proxyResponse.Payload = string(response.Body)
+ }
+ switch response.Metadata["call-context"] {
+ case "TargetProvider-Get":
+ if proxyResponse.IsOK {
+ var ret []model.ComponentSpec
+ err = json.Unmarshal(response.Body, &ret)
+ if err != nil {
+ sLog.Errorf(" P (MQTT Target): faild to deserialize components from MQTT - %+v, %s", err.Error(), string(response.Body))
+ }
+ proxyResponse.Payload = ret
+ }
+ i.GetChan <- proxyResponse
+ case "TargetProvider-Remove":
+ i.RemoveChan <- proxyResponse
+ case "TargetProvider-NeedsUpdate":
+ i.NeedsUpdateChan <- proxyResponse
+ case "TargetProvider-NeedsRemove":
+ i.NeedsRemoveChan <- proxyResponse
+ case "TargetProvider-Apply":
+ i.ApplyChan <- proxyResponse
+ }
+ }); token.Wait() && token.Error() != nil {
+ if token.Error().Error() != "subscription exists" {
+ sLog.Errorf(" P (MQTT Target): faild to connect to subscribe to the response topic - %+v", token.Error())
+ err = v1alpha2.NewCOAError(token.Error(), "failed to subscribe to response topic", v1alpha2.InternalError)
+ return err
+ }
+ }
+ i.Initialized = true
+ return nil
+}
+func toMQTTTargetProviderConfig(config providers.IProviderConfig) (MQTTTargetProviderConfig, error) {
+ ret := MQTTTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+func (i *MQTTTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ _, span := observability.StartSpan("MQTT Target Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (MQTT Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ data, _ := json.Marshal(deployment)
+ request := v1alpha2.COARequest{
+ Route: "instances",
+ Method: "GET",
+ Body: data,
+ Metadata: map[string]string{
+ "call-context": "TargetProvider-Get",
+ },
+ }
+ data, _ = json.Marshal(request)
+
+ if token := i.MQTTClient.Publish(i.Config.RequestTopic, 0, false, data); token.Wait() && token.Error() != nil {
+ sLog.Infof(" P (MQTT Target): failed to getting artifacts - %s", token.Error())
+ err = token.Error()
+ return nil, err
+ }
+
+ timeout := time.After(time.Duration(i.Config.TimeoutSeconds) * time.Second)
+ select {
+ case resp := <-i.GetChan:
+ if resp.IsOK {
+ var data []byte
+ data, err = json.Marshal(resp.Payload)
+ if err != nil {
+ sLog.Infof(" P (MQTT Target): failed to serialize payload - %s - %s", err.Error(), fmt.Sprint(resp.Payload))
+ err = v1alpha2.NewCOAError(nil, err.Error(), v1alpha2.InternalError)
+ return nil, err
+ }
+ var ret []model.ComponentSpec
+ err = json.Unmarshal(data, &ret)
+ if err != nil {
+ sLog.Infof(" P (MQTT Target): failed to deserialize components - %s - %s", err.Error(), fmt.Sprint(data))
+ err = v1alpha2.NewCOAError(nil, err.Error(), v1alpha2.InternalError)
+ return nil, err
+ }
+ return ret, nil
+ } else {
+ err = v1alpha2.NewCOAError(nil, fmt.Sprint(resp.Payload), resp.State)
+ return nil, err
+ }
+ case <-timeout:
+ err = v1alpha2.NewCOAError(nil, "didn't get response to Get() call over MQTT", v1alpha2.InternalError)
+ return nil, err
+ }
+}
+func (i *MQTTTargetProvider) Remove(ctx context.Context, deployment model.DeploymentSpec, currentRef []model.ComponentSpec) error {
+ _, span := observability.StartSpan("MQTT Target Provider", ctx, &map[string]string{
+ "method": "Remove",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof(" P (MQTT Target): deleting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ data, _ := json.Marshal(deployment)
+ request := v1alpha2.COARequest{
+ Route: "instances",
+ Method: "DELETE",
+ Body: data,
+ Metadata: map[string]string{
+ "call-context": "TargetProvider-Remove",
+ },
+ }
+ data, _ = json.Marshal(request)
+
+ if token := i.MQTTClient.Publish(i.Config.RequestTopic, 0, false, data); token.Wait() && token.Error() != nil {
+ err = token.Error()
+ return err
+ }
+
+ timeout := time.After(time.Duration(i.Config.TimeoutSeconds) * time.Second)
+ select {
+ case resp := <-i.RemoveChan:
+ if resp.IsOK {
+ err = nil
+ return err
+ } else {
+ err = v1alpha2.NewCOAError(nil, fmt.Sprint(resp.Payload), resp.State)
+ return err
+ }
+ case <-timeout:
+ err = v1alpha2.NewCOAError(nil, "didn't get response to Remove() call over MQTT", v1alpha2.InternalError)
+ return err
+ }
+}
+
+func (i *MQTTTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("MQTT Target Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof(" P (MQTT Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+ data, _ := json.Marshal(deployment)
+
+ components = step.GetUpdatedComponents()
+ if len(components) > 0 {
+
+ request := v1alpha2.COARequest{
+ Route: "instances",
+ Method: "POST",
+ Body: data,
+ Metadata: map[string]string{
+ "call-context": "TargetProvider-Apply",
+ },
+ }
+ data, _ = json.Marshal(request)
+
+ if token := i.MQTTClient.Publish(i.Config.RequestTopic, 0, false, data); token.Wait() && token.Error() != nil {
+ err = token.Error()
+ return ret, err
+ }
+
+ timeout := time.After(time.Duration(i.Config.TimeoutSeconds) * time.Second)
+ select {
+ case resp := <-i.ApplyChan:
+ if resp.IsOK {
+ err = nil
+ return ret, err
+ } else {
+ err = v1alpha2.NewCOAError(nil, fmt.Sprint(resp.Payload), resp.State)
+ return ret, err
+ }
+ case <-timeout:
+ err = v1alpha2.NewCOAError(nil, "didn't get response to Apply() call over MQTT", v1alpha2.InternalError)
+ return ret, err
+ }
+ }
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ request := v1alpha2.COARequest{
+ Route: "instances",
+ Method: "DELETE",
+ Body: data,
+ Metadata: map[string]string{
+ "call-context": "TargetProvider-Remove",
+ },
+ }
+ data, _ = json.Marshal(request)
+
+ if token := i.MQTTClient.Publish(i.Config.RequestTopic, 0, false, data); token.Wait() && token.Error() != nil {
+ err = token.Error()
+ return ret, err
+ }
+
+ timeout := time.After(time.Duration(i.Config.TimeoutSeconds) * time.Second)
+ select {
+ case resp := <-i.RemoveChan:
+ if resp.IsOK {
+ err = nil
+ return ret, err
+ } else {
+ err = v1alpha2.NewCOAError(nil, fmt.Sprint(resp.Payload), resp.State)
+ return ret, err
+ }
+ case <-timeout:
+ err = v1alpha2.NewCOAError(nil, "didn't get response to Remove() call over MQTT", v1alpha2.InternalError)
+ return ret, err
+ }
+ }
+ //TODO: Should we remove empty namespaces?
+ err = nil
+ return ret, nil
+}
+
+func (*MQTTTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ }
+}
+
+type TwoComponentSlices struct {
+ Current []model.ComponentSpec `json:"current"`
+ Desired []model.ComponentSpec `json:"desired"`
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package proxy
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "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/apis/v1alpha2/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type ProxyUpdateProviderConfig struct {
+ Name string `json:"name"`
+ ServerURL string `json:"serverUrl"`
+}
+
+type ProxyUpdateProvider struct {
+ Config ProxyUpdateProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func ProxyUpdateProviderConfigFromMap(properties map[string]string) (ProxyUpdateProviderConfig, error) {
+ ret := ProxyUpdateProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = utils.ParseProperty(v)
+ }
+ if v, ok := properties["serverUrl"]; ok {
+ ret.ServerURL = utils.ParseProperty(v)
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "proxy update provider server url is not set", v1alpha2.BadConfig)
+ }
+ return ret, nil
+}
+
+func (i *ProxyUpdateProvider) InitWithMap(properties map[string]string) error {
+ config, err := ProxyUpdateProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *ProxyUpdateProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *ProxyUpdateProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("Proxy Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+ sLog.Info("~~~ Proxy Provider ~~~ : Init()")
+
+ updateConfig, err := toProxyUpdateProviderConfig(config)
+ if err != nil {
+ err = errors.New("expected ProxyUpdateProviderConfig")
+ return err
+ }
+ i.Config = updateConfig
+
+ return nil
+}
+func toProxyUpdateProviderConfig(config providers.IProviderConfig) (ProxyUpdateProviderConfig, error) {
+ ret := ProxyUpdateProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ ret.Name = utils.ParseProperty(ret.Name)
+ ret.ServerURL = utils.ParseProperty(ret.ServerURL)
+ return ret, err
+}
+
+func (a *ProxyUpdateProvider) callRestAPI(route string, method string, payload []byte) ([]byte, error) {
+ client := &http.Client{}
+ url := a.Config.ServerURL + route
+ req, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
+ if err != nil {
+ return nil, v1alpha2.NewCOAError(err, fmt.Sprintf("failed to invoke Percept API: %v", err), v1alpha2.InternalError)
+ }
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, v1alpha2.NewCOAError(err, fmt.Sprintf("failed to invoke Percept API: %v", err), v1alpha2.InternalError)
+ }
+ defer resp.Body.Close()
+ bodyBytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, v1alpha2.NewCOAError(err, fmt.Sprintf("failed to invoke Percept API: %v", err), v1alpha2.InternalError)
+ }
+ if resp.StatusCode >= 300 {
+ return nil, v1alpha2.NewCOAError(err, fmt.Sprintf("failed to invoke Percept API: %v", string(bodyBytes)), v1alpha2.InternalError)
+ }
+ return bodyBytes, nil
+}
+
+func (i *ProxyUpdateProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ _, span := observability.StartSpan("Proxy Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof("~~~ Proxy Provider ~~~ : getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ data, _ := json.Marshal(deployment)
+ payload, err := i.callRestAPI("instances", "GET", data)
+ if err != nil {
+ return nil, err
+ }
+ ret := make([]model.ComponentSpec, 0)
+ err = json.Unmarshal(payload, &ret)
+ if err != nil {
+ return nil, err
+ }
+
+ return ret, nil
+}
+
+func (i *ProxyUpdateProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("Proxy Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof("~~~ Proxy Provider ~~~ : applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+ components = step.GetUpdatedComponents()
+ if len(components) > 0 {
+ data, _ := json.Marshal(deployment)
+
+ _, err = i.callRestAPI("instances", "POST", data)
+ if err != nil {
+ return ret, err
+ }
+ if err != nil {
+ return ret, err
+ }
+ }
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ data, _ := json.Marshal(deployment)
+ _, err = i.callRestAPI("instances", "DELETE", data)
+ if err != nil {
+ return ret, err
+ }
+ }
+ //TODO: Should we remove empty namespaces?
+ err = nil
+ return ret, nil
+}
+
+func (*ProxyUpdateProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ }
+}
+
+type TwoComponentSlices struct {
+ Current []model.ComponentSpec `json:"current"`
+ Desired []model.ComponentSpec `json:"desired"`
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package script
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+
+ "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"
+ "github.com/google/uuid"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type ScriptProviderConfig struct {
+ Name string `json:"name"`
+ ApplyScript string `json:"applyScript"`
+ RemoveScript string `json:"removeScript"`
+ GetScript string `json:"getScript"`
+ ScriptFolder string `json:"scriptFolder,omitempty"`
+ StagingFolder string `json:"stagingFolder,omitempty"`
+ ScriptEngine string `json:"scriptEngine,omitempty"`
+}
+
+type ScriptProvider struct {
+ Config ScriptProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func ScriptProviderConfigFromMap(properties map[string]string) (ScriptProviderConfig, error) {
+ ret := ScriptProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["stagingFolder"]; ok {
+ ret.StagingFolder = v
+ }
+ if v, ok := properties["scriptFolder"]; ok {
+ ret.ScriptFolder = v
+ }
+ if v, ok := properties["applyScript"]; ok {
+ ret.ApplyScript = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "invalid script provider config, exptected 'applyScript'", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["removeScript"]; ok {
+ ret.RemoveScript = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "invalid script provider config, exptected 'removeScript'", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["getScript"]; ok {
+ ret.GetScript = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "invalid script provider config, exptected 'getScript'", v1alpha2.BadConfig)
+ }
+ if v, ok := properties["scriptEngine"]; ok {
+ ret.ScriptEngine = v
+ } else {
+ ret.ScriptEngine = "bash"
+ }
+ if ret.ScriptEngine != "bash" && ret.ScriptEngine != "powershell" {
+ return ret, v1alpha2.NewCOAError(nil, "invalid script engine, exptected 'bash' or 'powershell'", v1alpha2.BadConfig)
+ }
+ return ret, nil
+}
+func (i *ScriptProvider) InitWithMap(properties map[string]string) error {
+ config, err := ScriptProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *ScriptProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *ScriptProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("Script Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (Script Target): Init()")
+
+ updateConfig, err := toScriptProviderConfig(config)
+ if err != nil {
+ err = errors.New("expected ScriptProviderConfig")
+ return err
+ }
+ i.Config = updateConfig
+
+ if strings.HasPrefix(i.Config.ScriptFolder, "http") {
+ err = downloadFile(i.Config.ScriptFolder, i.Config.ApplyScript, i.Config.StagingFolder)
+ if err != nil {
+ return err
+ }
+ err = downloadFile(i.Config.ScriptFolder, i.Config.RemoveScript, i.Config.StagingFolder)
+ if err != nil {
+ return err
+ }
+ err = downloadFile(i.Config.ScriptFolder, i.Config.GetScript, i.Config.StagingFolder)
+ if err != nil {
+ return err
+ }
+ }
+
+ err = nil
+ return nil
+}
+func downloadFile(scriptFolder string, script string, stagingFolder string) error {
+ sPath, err := url.JoinPath(scriptFolder, script)
+ if err != nil {
+ return err
+ }
+ tPath := filepath.Join(stagingFolder, script)
+
+ out, err := os.Create(tPath)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ resp, err := http.Get(sPath)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ _, err = io.Copy(out, resp.Body)
+ if err != nil {
+ return err
+ }
+ return os.Chmod(tPath, 0755)
+}
+func toScriptProviderConfig(config providers.IProviderConfig) (ScriptProviderConfig, error) {
+ ret := ScriptProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+
+func (i *ScriptProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ _, span := observability.StartSpan("Script Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof(" P (Script Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ id := uuid.New().String()
+ input := id + ".json"
+ input_ref := id + "-ref.json"
+ output := id + "-output.json"
+
+ staging := filepath.Join(i.Config.StagingFolder, input)
+ file, _ := json.MarshalIndent(deployment, "", " ")
+ _ = ioutil.WriteFile(staging, file, 0644)
+
+ staging_ref := filepath.Join(i.Config.StagingFolder, input_ref)
+ file_ref, _ := json.MarshalIndent(references, "", " ")
+ _ = ioutil.WriteFile(staging_ref, file_ref, 0644)
+
+ abs, _ := filepath.Abs(staging)
+ abs_ref, _ := filepath.Abs(staging_ref)
+
+ defer os.Remove(abs)
+ defer os.Remove(abs_ref)
+
+ scriptAbs, _ := filepath.Abs(filepath.Join(i.Config.ScriptFolder, i.Config.GetScript))
+ if strings.HasPrefix(i.Config.ScriptFolder, "http") {
+ scriptAbs, _ = filepath.Abs(filepath.Join(i.Config.StagingFolder, i.Config.GetScript))
+ }
+
+ o, err := i.runCommand(scriptAbs, abs, abs_ref)
+ sLog.Debugf(" P (Script Target): get script output: %s", o)
+
+ if err != nil {
+ sLog.Errorf(" P (Script Target): failed to run get script: %+v", err)
+ return nil, err
+ }
+
+ outputStaging := filepath.Join(i.Config.StagingFolder, output)
+
+ data, err := ioutil.ReadFile(outputStaging)
+
+ if err != nil {
+ sLog.Errorf(" P (Script Target): failed to parse get script output (expected []ComponentSpec): %+v", err)
+ return nil, err
+ }
+
+ abs_output, _ := filepath.Abs(outputStaging)
+
+ defer os.Remove(abs_output)
+
+ ret := make([]model.ComponentSpec, 0)
+ err = json.Unmarshal(data, &ret)
+ if err != nil {
+ sLog.Errorf(" P (Script Target): failed to parse get script output (expected []ComponentSpec): %+v", err)
+ return nil, err
+ }
+ return ret, nil
+}
+func (i *ScriptProvider) runScriptOnComponents(deployment model.DeploymentSpec, components []model.ComponentSpec, isRemove bool) (map[string]model.ComponentResultSpec, error) {
+ id := uuid.New().String()
+ deploymentId := id + ".json"
+ currenRefId := id + "-ref.json"
+ output := id + "-output.json"
+
+ stagingDeployment := filepath.Join(i.Config.StagingFolder, deploymentId)
+ file, _ := json.MarshalIndent(deployment, "", " ")
+ _ = ioutil.WriteFile(stagingDeployment, file, 0644)
+
+ stagingRef := filepath.Join(i.Config.StagingFolder, currenRefId)
+ file, _ = json.MarshalIndent(components, "", " ")
+ _ = ioutil.WriteFile(stagingRef, file, 0644)
+
+ absDeployment, _ := filepath.Abs(stagingDeployment)
+ absRef, _ := filepath.Abs(stagingRef)
+
+ var scriptAbs = ""
+ if isRemove {
+ scriptAbs, _ = filepath.Abs(filepath.Join(i.Config.ScriptFolder, i.Config.RemoveScript))
+ if strings.HasPrefix(i.Config.ScriptFolder, "http") {
+ scriptAbs, _ = filepath.Abs(filepath.Join(i.Config.StagingFolder, i.Config.RemoveScript))
+ }
+ } else {
+ scriptAbs, _ = filepath.Abs(filepath.Join(i.Config.ScriptFolder, i.Config.ApplyScript))
+ if strings.HasPrefix(i.Config.ScriptFolder, "http") {
+ scriptAbs, _ = filepath.Abs(filepath.Join(i.Config.StagingFolder, i.Config.ApplyScript))
+ }
+ }
+ o, err := i.runCommand(scriptAbs, absDeployment, absRef)
+ sLog.Debugf(" P (Script Target): apply script output: %s", o)
+
+ defer os.Remove(absDeployment)
+ defer os.Remove(absRef)
+
+ if err != nil {
+ sLog.Errorf(" P (Script Target): failed to run apply script: %+v", err)
+ return nil, err
+ }
+
+ outputStaging := filepath.Join(i.Config.StagingFolder, output)
+
+ data, err := ioutil.ReadFile(outputStaging)
+
+ if err != nil {
+ sLog.Errorf(" P (Script Target): failed to parse apply script output (expected map[string]model.ComponentResultSpec): %+v", err)
+ return nil, err
+ }
+
+ abs_output, _ := filepath.Abs(outputStaging)
+
+ defer os.Remove(abs_output)
+
+ ret := make(map[string]model.ComponentResultSpec)
+ err = json.Unmarshal(data, &ret)
+ if err != nil {
+ sLog.Errorf(" P (Script Target): failed to parse get script output (expected map[string]model.ComponentResultSpec): %+v", err)
+ return nil, err
+ }
+ return ret, nil
+}
+func (i *ScriptProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("Script Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+ sLog.Infof(" P (Script Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ err = i.GetValidationRule(ctx).Validate([]model.ComponentSpec{}) //this provider doesn't handle any components TODO: is this right?
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+ components := step.GetUpdatedComponents()
+ if len(components) > 0 {
+ var retU map[string]model.ComponentResultSpec
+ retU, err = i.runScriptOnComponents(deployment, components, false)
+ if err != nil {
+ sLog.Errorf(" P (Script Target): failed to run apply script: %+v", err)
+ return nil, err
+ }
+ for k, v := range retU {
+ ret[k] = v
+ }
+ }
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ var retU map[string]model.ComponentResultSpec
+ retU, err = i.runScriptOnComponents(deployment, components, true)
+ if err != nil {
+ sLog.Errorf(" P (Script Target): failed to run remove script: %+v", err)
+ return nil, err
+ }
+ for k, v := range retU {
+ ret[k] = v
+ }
+ }
+ return ret, nil
+}
+func (*ScriptProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ }
+}
+
+func (i *ScriptProvider) runCommand(scriptAbs string, parameters ...string) ([]byte, error) {
+ // Sanitize input to prevent command injection
+ scriptAbs = strings.ReplaceAll(scriptAbs, "|", "")
+ scriptAbs = strings.ReplaceAll(scriptAbs, "&", "")
+ for idx, param := range parameters {
+ parameters[idx] = strings.ReplaceAll(param, "|", "")
+ parameters[idx] = strings.ReplaceAll(param, "&", "")
+ }
+
+ var err error
+ var out []byte
+ params := make([]string, 0)
+ if i.Config.ScriptEngine == "" || i.Config.ScriptEngine == "bash" {
+ params = append(params, parameters...)
+ out, err = exec.Command(scriptAbs, params...).Output()
+ } else {
+ params = append(params, scriptAbs)
+ params = append(params, parameters...)
+ out, err = exec.Command("powershell", params...).Output()
+ }
+ return out, err
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package staging
+
+import (
+ "context"
+ "encoding/json"
+
+ "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/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"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type StagingTargetProviderConfig struct {
+ Name string `json:"name"`
+ TargetName string `json:"targetName"`
+}
+
+type StagingTargetProvider struct {
+ Config StagingTargetProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func StagingProviderConfigFromMap(properties map[string]string) (StagingTargetProviderConfig, error) {
+ ret := StagingTargetProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["targetName"]; ok {
+ ret.TargetName = v
+ } else {
+ return ret, v1alpha2.NewCOAError(nil, "invalid staging provider config, exptected 'targetName'", v1alpha2.BadConfig)
+ }
+ return ret, nil
+}
+
+func (i *StagingTargetProvider) InitWithMap(properties map[string]string) error {
+ config, err := StagingProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *StagingTargetProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *StagingTargetProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("Staging Target Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+ sLog.Info(" P (Staging Target): Init()")
+
+ updateConfig, err := toStagingTargetProviderConfig(config)
+ if err != nil {
+ sLog.Errorf(" P (Staging Target): expected StagingTargetProviderConfig: %+v", err)
+ return err
+ }
+ i.Config = updateConfig
+ return nil
+}
+func toStagingTargetProviderConfig(config providers.IProviderConfig) (StagingTargetProviderConfig, error) {
+ ret := StagingTargetProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *StagingTargetProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ ctx, span := observability.StartSpan("Staging Target Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ sLog.Infof(" P (Staging Target): getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ var err error
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ scope := deployment.Instance.Scope
+ if scope == "" {
+ scope = "default"
+ }
+ catalog, err := utils.GetCatalog(
+ ctx,
+ i.Context.SiteInfo.CurrentSite.BaseUrl,
+ deployment.Instance.Name+"-"+i.Config.TargetName,
+ i.Context.SiteInfo.CurrentSite.Username,
+ i.Context.SiteInfo.CurrentSite.Password)
+
+ if err != nil {
+ if v1alpha2.IsNotFound(err) {
+ sLog.Infof(" P (Staging Target): no staged artifact found")
+ return nil, nil
+ }
+ sLog.Errorf(" P (Staging Target): failed to get staged artifact: %v", err)
+ return nil, err
+ }
+
+ if spec, ok := catalog.Spec.Properties["components"]; ok {
+ var components []model.ComponentSpec
+ jData, _ := json.Marshal(spec)
+ err = json.Unmarshal(jData, &components)
+ if err != nil {
+ sLog.Errorf(" P (Staging Target): failed to get staged artifact: %v", err)
+ return nil, err
+ }
+ ret := make([]model.ComponentSpec, len(references))
+ for i, reference := range references {
+ for _, component := range components {
+ if component.Name == reference.Component.Name {
+ ret[i] = component
+ break
+ }
+ }
+ }
+ return ret, nil
+ }
+ err = v1alpha2.NewCOAError(nil, "staged artifact is not found as a 'spec' property", v1alpha2.NotFound)
+ sLog.Errorf(" P (Staging Target): failed to get staged artifact: %v", err)
+ return nil, err
+}
+func (i *StagingTargetProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("Staging Target Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ sLog.Infof(" P (Staging Target): applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ var err error
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ err = i.GetValidationRule(ctx).Validate([]model.ComponentSpec{}) //this provider doesn't handle any components TODO: is this right?
+ if err != nil {
+ sLog.Errorf(" P (Staging Target): failed to validate components: %v", err)
+ return nil, err
+ }
+ if isDryRun {
+ sLog.Infof(" P (Staging Target): dry run, skipping apply")
+ return nil, nil
+ }
+ ret := step.PrepareResultMap()
+
+ scope := deployment.Instance.Scope
+ if scope == "" {
+ scope = "default"
+ }
+
+ var catalog model.CatalogState
+
+ catalog, err = utils.GetCatalog(
+ ctx,
+ i.Context.SiteInfo.CurrentSite.BaseUrl,
+ deployment.Instance.Name+"-"+i.Config.TargetName,
+ i.Context.SiteInfo.CurrentSite.Username,
+ i.Context.SiteInfo.CurrentSite.Password)
+ if err != nil && !v1alpha2.IsNotFound(err) {
+ sLog.Errorf(" P (Staging Target): failed to get staged artifact: %v", err)
+ return ret, err
+ }
+
+ if catalog.Spec == nil {
+ catalog.Id = deployment.Instance.Name + "-" + i.Config.TargetName
+ catalog.Spec = &model.CatalogSpec{
+ SiteId: i.Context.SiteInfo.SiteId,
+ Type: "staged",
+ Name: catalog.Id,
+ }
+ }
+ if catalog.Spec.Properties == nil {
+ catalog.Spec.Properties = make(map[string]interface{})
+ }
+
+ var existing []model.ComponentSpec
+ if v, ok := catalog.Spec.Properties["components"]; ok {
+ jData, _ := json.Marshal(v)
+ err = json.Unmarshal(jData, &existing)
+ if err != nil {
+ sLog.Errorf(" P (Staging Target): failed to get staged artifact: %v", err)
+ return ret, err
+ }
+ }
+
+ components := step.GetUpdatedComponents()
+ if len(components) > 0 {
+ for i, component := range components {
+ found := false
+ for j, c := range existing {
+ if c.Name == component.Name {
+ found = true
+ existing[j] = components[i]
+ ret[component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.Updated,
+ Message: "",
+ }
+ break
+ }
+ }
+ if !found {
+ existing = append(existing, component)
+ }
+ }
+ }
+
+ var deleted []model.ComponentSpec
+ if v, ok := catalog.Spec.Properties["removed-components"]; ok {
+ jData, _ := json.Marshal(v)
+ err = json.Unmarshal(jData, &deleted)
+ if err != nil {
+ sLog.Errorf(" P (Staging Target): failed to get staged artifact: %v", err)
+ return ret, err
+ }
+ }
+
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ for i, component := range components {
+ found := false
+ for j, c := range deleted {
+ if c.Name == component.Name {
+ found = true
+ deleted[j] = components[i]
+ ret[component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.Updated,
+ Message: "",
+ }
+ break
+ }
+ }
+ if !found {
+ deleted = append(deleted, component)
+ }
+ }
+ }
+
+ catalog.Spec.Properties["components"] = existing
+ catalog.Spec.Properties["removed-components"] = deleted
+ jData, _ := json.Marshal(catalog.Spec)
+ err = utils.UpsertCatalog(
+ ctx,
+ i.Context.SiteInfo.CurrentSite.BaseUrl,
+ deployment.Instance.Name+"-"+i.Config.TargetName,
+ i.Context.SiteInfo.CurrentSite.Username,
+ i.Context.SiteInfo.CurrentSite.Password, jData)
+ if err != nil {
+ sLog.Errorf(" P (Staging Target): failed to upsert staged artifact: %v", err)
+ }
+ return ret, err
+}
+
+func (*StagingTargetProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package sideload
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "os/exec"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "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"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type Win10SideLoadProviderConfig struct {
+ Name string `json:"name"`
+ IPAddress string `json:"ipAddress"`
+ Pin string `json:"pin,omitempty"`
+ WinAppDeployCmdPath string `json:"winAppDeployCmdPath"`
+ NetworkUser string `json:"networkUser,omitempty"`
+ NetworkPassword string `json:"networkPassword,omitempty"`
+ Silent bool `json:"silent,omitempty"`
+}
+
+type Win10SideLoadProvider struct {
+ Config Win10SideLoadProviderConfig
+ Context *contexts.ManagerContext
+}
+
+func Win10SideLoadProviderConfigFromMap(properties map[string]string) (Win10SideLoadProviderConfig, error) {
+ ret := Win10SideLoadProviderConfig{}
+ if v, ok := properties["name"]; ok {
+ ret.Name = v
+ }
+ if v, ok := properties["ipAddress"]; ok {
+ ret.IPAddress = v
+ } else {
+ ret.IPAddress = "localhost"
+ }
+ if v, ok := properties["pin"]; ok {
+ ret.Pin = v
+ }
+ if v, ok := properties["winAppDeployCmdPath"]; ok {
+ ret.WinAppDeployCmdPath = v
+ } else {
+ ret.WinAppDeployCmdPath = "c:\\Program Files (x86)\\Windows Kits\\10\\bin\\10.0.19041.0\\x86\\WinAppDeployCmd.exe"
+ }
+ if v, ok := properties["networkUser"]; ok {
+ ret.NetworkUser = v
+ }
+ if v, ok := properties["networkPassword"]; ok {
+ ret.NetworkPassword = v
+ }
+ if v, ok := properties["silent"]; ok {
+ bVal, err := strconv.ParseBool(v)
+ if err != nil {
+ ret.Silent = false
+ } else {
+ ret.Silent = bVal
+ }
+ }
+ return ret, nil
+}
+func (i *Win10SideLoadProvider) InitWithMap(properties map[string]string) error {
+ config, err := Win10SideLoadProviderConfigFromMap(properties)
+ if err != nil {
+ return err
+ }
+ return i.Init(config)
+}
+
+func (s *Win10SideLoadProvider) SetContext(ctx *contexts.ManagerContext) {
+ s.Context = ctx
+}
+
+func (i *Win10SideLoadProvider) Init(config providers.IProviderConfig) error {
+ _, span := observability.StartSpan("Win 10 Sideload Provider", context.TODO(), &map[string]string{
+ "method": "Init",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info("~~~ Win 10 Sideload Provider ~~~ : Init()")
+
+ updateConfig, err := toWin10SideLoadProviderConfig(config)
+ if err != nil {
+ err = errors.New("expected Win10SideLoadProviderConfig")
+ return err
+ }
+ i.Config = updateConfig
+
+ return nil
+}
+func toWin10SideLoadProviderConfig(config providers.IProviderConfig) (Win10SideLoadProviderConfig, error) {
+ ret := Win10SideLoadProviderConfig{}
+ data, err := json.Marshal(config)
+ if err != nil {
+ return ret, err
+ }
+ err = json.Unmarshal(data, &ret)
+ return ret, err
+}
+func (i *Win10SideLoadProvider) Get(ctx context.Context, deployment model.DeploymentSpec, references []model.ComponentStep) ([]model.ComponentSpec, error) {
+ _, span := observability.StartSpan("Win 10 Sideload Provider", ctx, &map[string]string{
+ "method": "Get",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof("~~~ Win 10 Sideload Provider ~~~ : getting artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ params := make([]string, 0)
+ params = append(params, "list")
+ params = append(params, "-ip")
+ params = append(params, i.Config.IPAddress)
+ if i.Config.Pin != "" {
+ params = append(params, "-pin")
+ params = append(params, i.Config.Pin)
+ }
+
+ out, err := exec.Command(i.Config.WinAppDeployCmdPath, params...).Output()
+
+ if err != nil {
+ return nil, err
+ }
+ str := string(out)
+ lines := strings.Split(str, "\r\n")
+
+ desired := deployment.GetComponentSlice()
+
+ re := regexp.MustCompile(`^(\w+\.)+\w+$`)
+ ret := make([]model.ComponentSpec, 0)
+ for _, line := range lines {
+ if re.Match([]byte(line)) {
+ mLine := line
+ if strings.LastIndex(line, "__") > 0 {
+ mLine = line[:strings.LastIndex(line, "__")]
+ }
+ for _, component := range desired {
+ if component.Name == mLine {
+ ret = append(ret, model.ComponentSpec{
+ Name: line,
+ Type: "win.uwp",
+ })
+ }
+ }
+ }
+ }
+
+ return ret, nil
+}
+func (i *Win10SideLoadProvider) Apply(ctx context.Context, deployment model.DeploymentSpec, step model.DeploymentStep, isDryRun bool) (map[string]model.ComponentResultSpec, error) {
+ ctx, span := observability.StartSpan("Win 10 Sideload Provider", ctx, &map[string]string{
+ "method": "Apply",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Infof("~~~ Win 10 Sideload Provider ~~~ : applying artifacts: %s - %s", deployment.Instance.Scope, deployment.Instance.Name)
+
+ components := step.GetComponents()
+ err = i.GetValidationRule(ctx).Validate(components)
+ if err != nil {
+ return nil, err
+ }
+ if isDryRun {
+ err = nil
+ return nil, nil
+ }
+
+ ret := step.PrepareResultMap()
+ components = step.GetUpdatedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if path, ok := component.Properties["app.package.path"].(string); ok {
+ params := make([]string, 0)
+ params = append(params, "install")
+ params = append(params, "-ip")
+ params = append(params, i.Config.IPAddress)
+ if i.Config.Pin != "" {
+ params = append(params, "-pin")
+ params = append(params, i.Config.Pin)
+ }
+ params = append(params, "-file")
+ params = append(params, path)
+
+ cmd := exec.Command(i.Config.WinAppDeployCmdPath, params...)
+ err = cmd.Run()
+ if err != nil {
+ ret[component.Name] = model.ComponentResultSpec{
+ Status: v1alpha2.UpdateFailed,
+ Message: err.Error(),
+ }
+ if i.Config.Silent {
+ return ret, nil
+ } else {
+ return ret, err
+ }
+ }
+ }
+ }
+ }
+ components = step.GetDeletedComponents()
+ if len(components) > 0 {
+ for _, component := range components {
+ if component.Name != "" {
+ params := make([]string, 0)
+ params = append(params, "uninstall")
+ params = append(params, "-ip")
+ params = append(params, i.Config.IPAddress)
+ if i.Config.Pin != "" {
+ params = append(params, "-pin")
+ params = append(params, i.Config.Pin)
+ }
+ params = append(params, "-package")
+
+ name := component.Name
+
+ // TODO: this is broken due to the refactor, the current reference is no longer available
+ // for _, ref := range currentRef {
+ // if ref.Name == name || strings.HasPrefix(ref.Name, name) {
+ // name = ref.Name
+ // break
+ // }
+ // }
+
+ params = append(params, name)
+
+ cmd := exec.Command(i.Config.WinAppDeployCmdPath, params...)
+ err = cmd.Run()
+ if err != nil {
+ if i.Config.Silent {
+ return ret, nil
+ } else {
+ return ret, err
+ }
+ }
+
+ }
+ }
+ }
+ err = nil
+ return ret, nil
+}
+
+func (i *Win10SideLoadProvider) NeedsUpdate(ctx context.Context, desired []model.ComponentSpec, current []model.ComponentSpec) bool {
+ for _, d := range desired {
+ found := false
+ for _, c := range current {
+ if c.Name == d.Name || strings.HasPrefix(c.Name, d.Name) {
+ found = true
+ }
+ }
+ if !found {
+ return true
+ }
+ }
+ return false
+}
+func (i *Win10SideLoadProvider) NeedsRemove(ctx context.Context, desired []model.ComponentSpec, current []model.ComponentSpec) bool {
+ for _, d := range desired {
+ for _, c := range current {
+ if c.Name == d.Name || strings.HasPrefix(c.Name, d.Name) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (*Win10SideLoadProvider) GetValidationRule(ctx context.Context) model.ValidationRule {
+ return model.ValidationRule{
+ RequiredProperties: []string{},
+ OptionalProperties: []string{},
+ RequiredComponentType: "",
+ RequiredMetadata: []string{},
+ OptionalMetadata: []string{},
+ ChangeDetectionProperties: []model.PropertyDesc{
+ {Name: "", IsComponentName: true, IgnoreCase: true, PrefixMatch: true},
+ },
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package utils
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "math"
+ "reflect"
+ "regexp"
+ "strconv"
+ "strings"
+ "text/scanner"
+
+ "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/utils"
+)
+
+type Token int
+
+const (
+ EOF Token = iota
+ NUMBER
+ INT
+ DOLLAR
+ IDENT
+ OPAREN
+ CPAREN
+ OBRACKET
+ CBRACKET
+ OCURLY
+ CCURLY
+ PLUS
+ MINUS
+ MULT
+ DIV
+ COMMA
+ PERIOD
+ COLON
+ QUESTION
+ EQUAL
+ STRING
+ RUNON
+ AMPHERSAND
+ SLASH
+ TILDE
+)
+
+var opNames = map[Token]string{
+ PLUS: "+",
+ MINUS: "-",
+ MULT: "*",
+ DIV: "/",
+ SLASH: "\\",
+ COMMA: ",",
+ PERIOD: ".",
+ COLON: ":",
+ QUESTION: "?",
+ EQUAL: "=",
+ AMPHERSAND: "&",
+ TILDE: "~",
+}
+
+type Node interface {
+ Eval(context utils.EvaluationContext) (interface{}, error)
+}
+
+type NumberNode struct {
+ Value float64
+}
+
+func (n *NumberNode) Eval(context utils.EvaluationContext) (interface{}, error) {
+ return n.Value, nil
+}
+
+type IntNode struct {
+ Value int64
+}
+
+func (n *IntNode) Eval(context utils.EvaluationContext) (interface{}, error) {
+ return n.Value, nil
+}
+
+type IdentifierNode struct {
+ Value string
+}
+
+func removeQuotes(s string) string {
+ if len(s) < 2 {
+ return s
+ }
+ first := s[0]
+ last := s[len(s)-1]
+ if first == '\'' && last == '\'' {
+ return s[1 : len(s)-1]
+ }
+ return s
+}
+
+func (n *IdentifierNode) Eval(context utils.EvaluationContext) (interface{}, error) {
+ return removeQuotes(n.Value), nil
+}
+
+type NullNode struct {
+}
+
+func (n *NullNode) Eval(context utils.EvaluationContext) (interface{}, error) {
+ return "", nil
+}
+
+type UnaryNode struct {
+ Op Token
+ Expr Node
+}
+
+func (n *UnaryNode) Eval(context utils.EvaluationContext) (interface{}, error) {
+ switch n.Op {
+ case PLUS:
+ if n.Expr != nil {
+ return n.Expr.Eval(context)
+ }
+ return "", nil
+ case MINUS:
+ if n.Expr != nil {
+ val, err := n.Expr.Eval(context)
+ if err != nil {
+ return val, err
+ }
+ if v, ok := val.(int64); ok {
+ return -v, nil
+ }
+ if v, ok := val.(float64); ok {
+ return -v, nil
+ }
+ return fmt.Sprintf("-%v", val), nil
+ }
+ return "", nil
+ case OBRACKET:
+ val, err := n.Expr.Eval(context)
+ if err != nil {
+ return val, err
+ }
+ return fmt.Sprintf("[%v]", val), nil
+ case OCURLY:
+ val, err := n.Expr.Eval(context)
+ if err != nil {
+ return val, err
+ }
+ return fmt.Sprintf("{%v}", val), nil
+ }
+ return nil, fmt.Errorf("operator '%s' is not allowed in this context", opNames[n.Op])
+}
+
+type BinaryNode struct {
+ Op Token
+ Left Node
+ Right Node
+}
+
+func (n *BinaryNode) Eval(context utils.EvaluationContext) (interface{}, error) {
+ switch n.Op {
+ case PLUS:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return formatFloats(lv, rv, ""), nil
+ case MINUS:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return formatFloats(lv, rv, "-"), nil
+ case COMMA:
+ lv, le := n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ rv, re := n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ return fmt.Sprintf("%v,%v", lv, rv), nil
+ case MULT:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return formatFloats(lv, rv, "*"), nil
+ case DIV:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return formatFloats(lv, rv, "/"), nil
+ case SLASH:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return fmt.Sprintf("%v\\%v", lv, rv), nil
+ case PERIOD:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return formatFloats(lv, rv, "."), nil
+ case COLON:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return fmt.Sprintf("%v:%v", lv, rv), nil
+ case QUESTION:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return fmt.Sprintf("%v?%v", lv, rv), nil
+ case EQUAL:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return fmt.Sprintf("%v=%v", lv, rv), nil
+ case AMPHERSAND:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return fmt.Sprintf("%v&%v", lv, rv), nil
+ case TILDE:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return fmt.Sprintf("%v~%v", lv, rv), nil
+ case RUNON:
+ var lv interface{} = ""
+ var le error
+ if n.Left != nil {
+ lv, le = n.Left.Eval(context)
+ if le != nil {
+ return nil, le
+ }
+ }
+ var rv interface{} = ""
+ var re error
+ if n.Right != nil {
+ rv, re = n.Right.Eval(context)
+ if re != nil {
+ return nil, re
+ }
+ }
+ return fmt.Sprintf("%v%v", lv, rv), nil
+ }
+
+ return nil, fmt.Errorf("operator '%s' is not allowed in this context", opNames[n.Op])
+}
+
+type FunctionNode struct {
+ Name string
+ Args []Node
+}
+
+func readProperty(properties map[string]string, key string) (string, error) {
+ if v, ok := properties[key]; ok {
+ return v, nil
+ }
+ return "", fmt.Errorf("property %s is not found", key)
+}
+func readPropertyInterface(properties map[string]interface{}, key string) (interface{}, error) {
+ if v, ok := properties[key]; ok {
+ return v, nil
+ }
+ return "", fmt.Errorf("property %s is not found", key)
+}
+func readArgument(deployment model.DeploymentSpec, component string, key string) (string, error) {
+
+ arguments := deployment.Instance.Arguments
+ if ca, ok := arguments[component]; ok {
+ if a, ok := ca[key]; ok {
+ return a, nil
+ }
+ }
+ components := deployment.Solution.Components
+ for _, c := range components {
+ if c.Name == component {
+ if v, ok := c.Parameters[key]; ok {
+ return v, nil
+ }
+ }
+ }
+ return "", fmt.Errorf("parameter %s is not found on component %s", key, component)
+}
+
+func toIntIfPossible(f float64) interface{} {
+ i := int64(f)
+ if float64(i) == f {
+ return i
+ }
+ return f
+}
+
+func formatFloats(left interface{}, right interface{}, operator string) interface{} {
+ var lv_f, rv_f float64
+ var okl, okr bool
+ if lv_i, ok := left.(int64); ok {
+ lv_f = float64(lv_i)
+ okl = true
+ } else {
+ lv_f, okl = left.(float64)
+ }
+ if rv_i, ok := right.(int64); ok {
+ rv_f = float64(rv_i)
+ okr = true
+ } else {
+ rv_f, okr = right.(float64)
+ }
+ if okl && okr {
+ switch operator {
+ case "":
+ return toIntIfPossible(lv_f + rv_f)
+ case "-":
+ return toIntIfPossible(lv_f - rv_f)
+ case "*":
+ return toIntIfPossible(lv_f * rv_f)
+ case "/":
+ if rv_f != 0 {
+ return toIntIfPossible(lv_f / rv_f)
+ } else {
+ lv_str := strconv.FormatFloat(lv_f, 'f', -1, 64)
+ rv_str := strconv.FormatFloat(rv_f, 'f', -1, 64)
+ return fmt.Sprintf("%v%s%v", lv_str, operator, rv_str)
+ }
+ case ".":
+ lv_str := strconv.FormatFloat(lv_f, 'f', -1, 64)
+ rv_str := strconv.FormatFloat(rv_f, 'f', -1, 64)
+ return fmt.Sprintf("%v%s%v", lv_str, operator, rv_str)
+ default:
+ return fmt.Errorf("operator '%s' is not allowed in this context", operator)
+ }
+ } else if okl {
+ lv_str := strconv.FormatFloat(lv_f, 'f', -1, 64)
+ return fmt.Sprintf("%v%s%v", lv_str, operator, right)
+ } else if okr {
+ rv_str := strconv.FormatFloat(rv_f, 'f', -1, 64)
+ return fmt.Sprintf("%v%s%v", left, operator, rv_str)
+ } else {
+ return fmt.Sprintf("%v%s%v", left, operator, right)
+ }
+}
+
+func (n *FunctionNode) Eval(context utils.EvaluationContext) (interface{}, error) {
+ switch n.Name {
+ case "param":
+ if len(n.Args) == 1 {
+ if context.Component == "" {
+ return nil, errors.New("a component name is needed to evaluate $param()")
+ }
+ key, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if deploymentSpec, ok := context.DeploymentSpec.(model.DeploymentSpec); ok {
+ argument, err := readArgument(deploymentSpec, context.Component, key.(string))
+ if err != nil {
+ return nil, err
+ }
+ return argument, nil
+ }
+ return nil, errors.New("deployment spec is not found")
+ }
+ return nil, fmt.Errorf("$params() expects 1 argument, found %d", len(n.Args))
+ case "property":
+ if len(n.Args) == 1 {
+ if context.Properties == nil || len(context.Properties) == 0 {
+ return nil, errors.New("a property collection is needed to evaluate $property()")
+ }
+ key, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ property, err := readProperty(context.Properties, key.(string))
+ if err != nil {
+ return nil, err
+ }
+ return property, nil
+ }
+ return nil, fmt.Errorf("$property() expects 1 argument, found %d", len(n.Args))
+ case "input":
+ if len(n.Args) == 1 {
+ if context.Inputs == nil || len(context.Inputs) == 0 {
+ return nil, errors.New("an input collection is needed to evaluate $input()")
+ }
+ key, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ property, err := readPropertyInterface(context.Inputs, key.(string))
+ if err != nil {
+ return nil, err
+ }
+ return property, nil
+ }
+ return nil, fmt.Errorf("$input() expects 1 argument, found %d", len(n.Args))
+ case "output":
+ if len(n.Args) == 2 {
+ if context.Outputs == nil || len(context.Outputs) == 0 {
+ //return nil, errors.New("an output collection is needed to evaluate $output()")
+ return "", nil
+ }
+ step, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ key, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := context.Outputs[step.(string)]; !ok {
+ return nil, fmt.Errorf("step %s is not found in output collection", step.(string))
+ }
+ property, err := readPropertyInterface(context.Outputs[step.(string)], key.(string))
+ if err != nil {
+ return nil, err
+ }
+ return property, nil
+ }
+ return nil, fmt.Errorf("$output() expects 2 argument, found %d", len(n.Args))
+ case "equal":
+ if len(n.Args) == 2 {
+ v1, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ v2, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ return compareInterfaces(v1, v2), nil
+ }
+ return nil, fmt.Errorf("$equal() expects 2 arguments, found %d", len(n.Args))
+ case "and":
+ if len(n.Args) == 2 {
+ val1, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ val2, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ return andBools(val1, val2)
+ }
+ return nil, fmt.Errorf("$and() expects 2 arguments, found %d", len(n.Args))
+ case "or":
+ if len(n.Args) == 2 {
+ val1, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ val2, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ return orBools(val1, val2)
+ }
+ return nil, fmt.Errorf("$or() expects 2 arguments, found %d", len(n.Args))
+ case "not":
+ if len(n.Args) == 1 {
+ val, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ return notBool(val)
+ }
+ return nil, fmt.Errorf("$not() expects 1 argument, found %d", len(n.Args))
+ case "gt":
+ if len(n.Args) == 2 {
+ val1, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ val2, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if fVal1, ok1 := toNumber(val1); ok1 {
+ if fVal2, ok2 := toNumber((val2)); ok2 {
+ return fVal1 > fVal2, nil
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val2)
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val1)
+ }
+ return nil, fmt.Errorf("$gt() expects 2 arguments, found %d", len(n.Args))
+ case "ge":
+ if len(n.Args) == 2 {
+ val1, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ val2, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if fVal1, ok1 := toNumber(val1); ok1 {
+ if fVal2, ok2 := toNumber((val2)); ok2 {
+ return fVal1 >= fVal2, nil
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val2)
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val1)
+ }
+ return nil, fmt.Errorf("$ge() expects 2 arguments, found %d", len(n.Args))
+ case "if":
+ if len(n.Args) == 3 {
+ cond, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if fmt.Sprintf("%v", cond) == "true" {
+ return n.Args[1].Eval(context)
+ } else {
+ return n.Args[2].Eval(context)
+ }
+ }
+ return nil, fmt.Errorf("$if() expects 3 arguments, found %d", len(n.Args))
+ case "in":
+ if len(n.Args) >= 2 {
+ val, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ for i := 1; i < len(n.Args); i++ {
+ v, err := n.Args[i].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if fmt.Sprintf("%v", val) == fmt.Sprintf("%v", v) {
+ return true, nil
+ }
+ }
+ return false, nil
+ }
+ return nil, fmt.Errorf("$in() expects at least 2 arguments, found %d", len(n.Args))
+ case "lt":
+ if len(n.Args) == 2 {
+ val1, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ val2, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if fVal1, ok1 := toNumber(val1); ok1 {
+ if fVal2, ok2 := toNumber((val2)); ok2 {
+ return fVal1 < fVal2, nil
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val2)
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val1)
+ }
+ return nil, fmt.Errorf("$lt() expects 2 arguments, found %d", len(n.Args))
+ case "between":
+ if len(n.Args) == 3 {
+ val1, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ val2, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ val3, err := n.Args[2].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if fVal1, ok1 := toNumber(val1); ok1 {
+ if fVal2, ok2 := toNumber((val2)); ok2 {
+ if fVal3, ok2 := toNumber((val3)); ok2 {
+ return fVal1 >= fVal2 && fVal1 <= fVal3, nil
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val3)
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val2)
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val1)
+ }
+ return nil, fmt.Errorf("$le() expects 2 arguments, found %d", len(n.Args))
+ case "le":
+ if len(n.Args) == 2 {
+ val1, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ val2, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ if fVal1, ok1 := toNumber(val1); ok1 {
+ if fVal2, ok2 := toNumber((val2)); ok2 {
+ return fVal1 <= fVal2, nil
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val2)
+ }
+ return nil, fmt.Errorf("%v is not a valid number", val1)
+ }
+ return nil, fmt.Errorf("$le() expects 2 arguments, found %d", len(n.Args))
+ case "config":
+ if len(n.Args) >= 2 {
+ if context.ConfigProvider == nil {
+ return nil, errors.New("a config provider is needed to evaluate $config()")
+ }
+ obj, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ field, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+
+ var overlays []string
+ if len(n.Args) > 2 {
+ for i := 2; i < len(n.Args); i++ {
+ overlay, err := n.Args[i].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ overlays = append(overlays, overlay.(string))
+ }
+ }
+
+ return context.ConfigProvider.Get(obj.(string), field.(string), overlays, context)
+ }
+ return nil, fmt.Errorf("$config() expects 2 arguments, found %d", len(n.Args))
+ case "secret":
+ if len(n.Args) == 2 {
+ if context.SecretProvider == nil {
+ return nil, errors.New("a secret provider is needed to evaluate $secret()")
+ }
+ obj, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ field, err := n.Args[1].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ return context.SecretProvider.Get(obj.(string), field.(string))
+ }
+ return nil, fmt.Errorf("$secret() expects 2 arguments, found %d", len(n.Args))
+ case "instance":
+ if len(n.Args) == 0 {
+ if deploymentSpec, ok := context.DeploymentSpec.(model.DeploymentSpec); ok {
+ return deploymentSpec.Instance.Name, nil
+ }
+ return nil, errors.New("deployment spec is not found")
+ }
+ return nil, fmt.Errorf("$instance() expects 0 arguments, found %d", len(n.Args))
+ case "val", "context":
+ if len(n.Args) == 0 {
+ return context.Value, nil
+ }
+ if len(n.Args) == 1 {
+ obj, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ path := obj.(string)
+ if strings.HasPrefix(path, "$") || strings.HasPrefix(path, "{$") {
+ result, err := JsonPathQuery(context.Value, obj.(string))
+ if err != nil {
+ return nil, err
+ }
+ return result, nil
+ } else {
+ if mobj, ok := context.Value.(map[string]interface{}); ok {
+ if v, ok := mobj[path]; ok {
+ return v, nil
+ } else {
+ return nil, fmt.Errorf("key %s is not found in context value", path)
+ }
+ } else {
+ return nil, fmt.Errorf("context value '%v' is not a map", context.Value)
+ }
+ }
+ }
+ return nil, fmt.Errorf("$val() or $context() expects 0 or 1 argument, found %d", len(n.Args))
+ case "json":
+ if len(n.Args) == 1 {
+ val, err := n.Args[0].Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ jData, err := json.Marshal(val)
+ if err != nil {
+ return nil, err
+ }
+ return string(jData), nil
+ }
+ return nil, fmt.Errorf("$json() expects 1 argument, fount %d", len(n.Args))
+ }
+ return nil, fmt.Errorf("invalid function name: '%s'", n.Name)
+}
+
+type Parser struct {
+ Segments []string
+ OriginalText string
+}
+
+type ExpressionParser struct {
+ s *scanner.Scanner
+ token Token
+ text string
+}
+
+func NewParser(text string) *Parser {
+ re := regexp.MustCompile(`(\${{.*?}})`)
+ loc := re.FindAllStringIndex(text, -1)
+
+ segments := make([]string, 0, len(loc)*2+1)
+ start := 0
+ for _, l := range loc {
+ if start != l[0] {
+ segments = append(segments, text[start:l[0]])
+ }
+ segments = append(segments, text[l[0]:l[1]])
+ start = l[1]
+ }
+ if start < len(text) {
+ segments = append(segments, text[start:])
+ }
+
+ p := &Parser{
+ Segments: segments,
+ }
+ return p
+}
+
+func (p *Parser) Eval(context utils.EvaluationContext) (interface{}, error) {
+ results := make([]interface{}, 0)
+ for _, s := range p.Segments {
+ if strings.HasPrefix(s, "${{") && strings.HasSuffix(s, "}}") {
+ text := s[3 : len(s)-2]
+ parser := newExpressionParser(text)
+ n, err := parser.Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ results = append(results, n)
+ } else {
+ results = append(results, s)
+ }
+ }
+ if len(results) == 1 {
+ return results[0], nil
+ }
+ //join the results as string
+ var ret interface{}
+ for _, v := range results {
+ if ret == nil {
+ ret = fmt.Sprintf("%v", v)
+ } else {
+ ret = fmt.Sprintf("%v%v", ret, v)
+ }
+ }
+ return ret, nil
+}
+
+func newExpressionParser(text string) *ExpressionParser {
+ var s scanner.Scanner // TODO: this is mostly used to scan go code, we should use a custom scanner
+ s.Init(strings.NewReader(strings.TrimSpace(text)))
+ s.Mode = scanner.ScanIdents | scanner.ScanChars | scanner.ScanStrings | scanner.ScanInts
+ p := &ExpressionParser{
+ s: &s,
+ text: text,
+ }
+ p.next()
+ return p
+}
+
+func (p *ExpressionParser) Eval(context utils.EvaluationContext) (interface{}, error) {
+ var ret interface{}
+ for {
+ n, err := p.expr(false)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := n.(*NullNode); !ok {
+ v, r := n.Eval(context)
+ if r != nil {
+ return "", r
+ }
+ if vt, ok := v.([]string); ok {
+ if ret == nil {
+ ret = vt
+ } else if vr, o := ret.([]string); o {
+ vr = append(vr, vt...)
+ ret = vr
+ } else {
+ jData, _ := json.Marshal(v)
+ ret = fmt.Sprintf("%v%v", ret, string(jData))
+ }
+ } else if vt, ok := v.([]interface{}); ok {
+ if ret == nil {
+ ret = vt
+ } else if vr, o := ret.([]interface{}); o {
+ vr = append(vr, vt...)
+ ret = vr
+ } else {
+ jData, _ := json.Marshal(v)
+ ret = fmt.Sprintf("%v%v", ret, string(jData))
+ }
+ } else if vt, ok := v.(map[string]interface{}); ok {
+ if ret == nil {
+ ret = vt
+ } else if vr, o := ret.(map[string]interface{}); o {
+ for k, v := range vt {
+ vr[k] = v
+ }
+ ret = vr
+ } else {
+ jData, _ := json.Marshal(v)
+ ret = fmt.Sprintf("%v%v", ret, string(jData))
+ }
+ } else {
+ if ret == nil {
+ ret = v
+ } else {
+ ret = fmt.Sprintf("%v%v", ret, v)
+ }
+ }
+ } else {
+ return ret, nil
+ }
+ p.next()
+ }
+}
+
+func (p *ExpressionParser) next() {
+ p.token = p.scan()
+}
+
+func (p *ExpressionParser) scan() Token {
+ tok := p.s.Scan()
+ p.text = p.s.TokenText()
+ switch tok {
+ case scanner.EOF:
+ return EOF
+ case scanner.Float:
+ return NUMBER
+ case scanner.Ident:
+ return IDENT
+ case '$':
+ return DOLLAR
+ case '(':
+ return OPAREN
+ case ')':
+ return CPAREN
+ case '[':
+ return OBRACKET
+ case ']':
+ return CBRACKET
+ case '{':
+ return OCURLY
+ case '}':
+ return CCURLY
+ case '+':
+ return PLUS
+ case '-':
+ return MINUS
+ case '*':
+ return MULT
+ case '/':
+ return DIV
+ case '\\':
+ return SLASH
+ case ',':
+ return COMMA
+ case '.':
+ return PERIOD
+ case ':':
+ return COLON
+ case '?':
+ return QUESTION
+ case '=':
+ return EQUAL
+ case '&':
+ return AMPHERSAND
+ case '~':
+ return TILDE
+ }
+ if _, err := strconv.ParseInt(p.text, 10, 64); err == nil {
+ return INT
+ }
+
+ if _, err := strconv.ParseFloat(p.text, 64); err == nil {
+ return NUMBER
+ }
+ return IDENT
+}
+
+func (p *ExpressionParser) match(t Token) error {
+ if p.token == t {
+ p.next()
+ } else {
+ return fmt.Errorf("expected %T, got %s", t, p.text)
+ }
+ return nil
+}
+
+func (p *ExpressionParser) primary() (Node, error) {
+ switch p.token {
+ case INT:
+ v, _ := strconv.ParseInt(p.text, 10, 64)
+ p.next()
+ return &IntNode{v}, nil
+ case NUMBER:
+ v, _ := strconv.ParseFloat(p.text, 64)
+ p.next()
+ return &NumberNode{v}, nil
+ case DOLLAR:
+ return p.function()
+ case OPAREN:
+ p.next()
+ node, err := p.expr(false)
+ if err != nil {
+ return nil, err
+ }
+ expr := node
+ if err := p.match(CPAREN); err != nil {
+ return nil, err
+ }
+ return expr, nil
+ case OBRACKET:
+ p.next()
+ node, err := p.expr(false)
+ if err != nil {
+ return nil, err
+ }
+ bexpr := node
+ if err := p.match(CBRACKET); err != nil {
+ return nil, err
+ }
+ return &UnaryNode{OBRACKET, bexpr}, nil
+ case OCURLY:
+ p.next()
+ node, err := p.expr(false)
+ if err != nil {
+ return nil, err
+ }
+ cexpr := node
+ if err := p.match(CCURLY); err != nil {
+ return nil, err
+ }
+ return &UnaryNode{OCURLY, cexpr}, nil
+ case PLUS:
+ p.next()
+ node, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ return &UnaryNode{PLUS, node}, nil
+ case MINUS:
+ p.next()
+ node, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ return &UnaryNode{MINUS, node}, nil
+ case IDENT:
+ v := p.text
+ p.next()
+ return &IdentifierNode{v}, nil
+ }
+ return nil, nil
+}
+
+func (p *ExpressionParser) factor() (Node, error) {
+ node, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ for {
+ switch p.token {
+ case MULT:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{MULT, node, n}
+ case DIV:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{DIV, node, n}
+ case SLASH:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{SLASH, node, n}
+ case PERIOD:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{PERIOD, node, n}
+ case COLON:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{COLON, node, n}
+ case QUESTION:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{QUESTION, node, n}
+ case EQUAL:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{EQUAL, node, n}
+ case TILDE:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{TILDE, node, n}
+ case AMPHERSAND:
+ p.next()
+ n, err := p.primary()
+ if err != nil {
+ return nil, err
+ }
+ node = &BinaryNode{AMPHERSAND, node, n}
+ default:
+ return node, nil
+ }
+ }
+}
+
+func (p *ExpressionParser) expr(inFunc bool) (Node, error) {
+ node, err := p.factor()
+ if node == nil || err != nil {
+ return &NullNode{}, err
+ }
+ for {
+ switch p.token {
+ case PLUS:
+ p.next()
+ f, err := p.factor()
+ if err != nil {
+ return &NullNode{}, err
+ }
+ node = &BinaryNode{PLUS, node, f}
+ case MINUS:
+ p.next()
+ f, err := p.factor()
+ if err != nil {
+ return &NullNode{}, err
+ }
+ node = &BinaryNode{MINUS, node, f}
+ case COMMA:
+ if !inFunc {
+ p.next()
+ f, err := p.factor()
+ if err != nil {
+ return &NullNode{}, err
+ }
+ node = &BinaryNode{COMMA, node, f}
+ } else {
+ return node, nil
+ }
+ case OPAREN:
+ p.next()
+ node, err := p.expr(false)
+ if err != nil {
+ return nil, err
+ }
+ expr := node
+ if err := p.match(CPAREN); err != nil {
+ return nil, err
+ }
+ return expr, nil
+ default:
+ return node, nil
+ }
+ }
+}
+
+func (p *ExpressionParser) function() (Node, error) {
+ err := p.match(DOLLAR)
+ if err != nil {
+ return nil, err
+ }
+ name := p.text
+ err = p.match(IDENT)
+ if err != nil {
+ return nil, err
+ }
+ err = p.match(OPAREN)
+ if err != nil {
+ return nil, err
+ }
+ args := []Node{}
+ for p.token != CPAREN {
+ node, err := p.expr(true)
+ if err != nil {
+ return nil, err
+ }
+ if _, ok := node.(*NullNode); ok {
+ return nil, fmt.Errorf("invalid argument")
+ }
+ args = append(args, node)
+ if p.token == COMMA {
+ p.next()
+ }
+ }
+ err = p.match(CPAREN)
+ if err != nil {
+ return nil, err
+ }
+ return &FunctionNode{name, args}, nil
+}
+
+func EvaluateDeployment(context utils.EvaluationContext) (model.DeploymentSpec, error) {
+ if deploymentSpec, ok := context.DeploymentSpec.(model.DeploymentSpec); ok {
+ for ic, c := range deploymentSpec.Solution.Components {
+
+ val, err := evalProperties(context, c.Metadata)
+ if err != nil {
+ return deploymentSpec, err
+ }
+ if val != nil {
+ metadata, ok := val.(map[string]string)
+ if !ok {
+ return deploymentSpec, fmt.Errorf("metadata must be a map")
+ }
+ stringMap := make(map[string]string)
+ for k, v := range metadata {
+ stringMap[k] = fmt.Sprintf("%v", v)
+ }
+ deploymentSpec.Solution.Components[ic].Metadata = stringMap
+ }
+
+ val, err = evalProperties(context, c.Properties)
+ if err != nil {
+ return deploymentSpec, err
+ }
+ props, ok := val.(map[string]interface{})
+ if !ok {
+ return deploymentSpec, fmt.Errorf("properties must be a map")
+ }
+ deploymentSpec.Solution.Components[ic].Properties = props
+ }
+ return deploymentSpec, nil
+ }
+ return model.DeploymentSpec{}, errors.New("deployment spec is not found")
+}
+func compareInterfaces(a, b interface{}) bool {
+ if reflect.TypeOf(a) == reflect.TypeOf(b) {
+ switch a.(type) {
+ case int, int8, int16, int32, int64:
+ return a.(int64) == b.(int64)
+ case uint, uint8, uint16, uint32, uint64:
+ return a.(uint64) == b.(uint64)
+ case float32, float64:
+ return math.Abs(a.(float64)-b.(float64)) < 1e-9
+ case string:
+ return a.(string) == b.(string)
+ case bool:
+ return a.(bool) == b.(bool)
+ }
+ }
+ if aState, ok := a.(v1alpha2.State); ok {
+ a = int(aState)
+ }
+ if bState, ok := b.(v1alpha2.State); ok {
+ b = int(bState)
+ }
+ return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
+}
+func andBools(a, b interface{}) (bool, error) {
+ if aBool, ok := toBool(a); ok {
+ if bBool, ok := toBool(b); ok {
+ return aBool && bBool, nil
+ }
+ return false, fmt.Errorf("%v is not a boolean value", b)
+ }
+ return false, fmt.Errorf("%v is not a boolean value", a)
+}
+func orBools(a, b interface{}) (bool, error) {
+ if aBool, ok := toBool(a); ok {
+ if bBool, ok := toBool(b); ok {
+ return aBool || bBool, nil
+ }
+ return false, fmt.Errorf("%v is not a boolean value", b)
+ }
+ return false, fmt.Errorf("%v is not a boolean value", a)
+}
+func notBool(a interface{}) (bool, error) {
+ if aBool, ok := toBool(a); ok {
+ return !aBool, nil
+ }
+ return false, fmt.Errorf("%v is not a boolean value", a)
+}
+func toBool(val interface{}) (bool, bool) {
+ switch val := val.(type) {
+ case bool:
+ return val, true
+ case string:
+ boolVal, err := strconv.ParseBool(val)
+ if err == nil {
+ return boolVal, true
+ }
+ }
+ return false, false
+}
+func toNumber(val interface{}) (float64, bool) {
+ num, err := strconv.ParseFloat(fmt.Sprintf("%v", val), 64)
+ if err == nil {
+ return num, true
+ }
+ return 0, false
+}
+func evalProperties(context utils.EvaluationContext, properties interface{}) (interface{}, error) {
+ switch p := properties.(type) {
+ case map[string]string:
+ for k, v := range p {
+ val, err := evalProperties(context, v)
+ if err != nil {
+ return nil, err
+ }
+ p[k] = FormatAsString(val)
+ }
+ case map[string]interface{}:
+ for k, v := range p {
+ val, err := evalProperties(context, v)
+ if err != nil {
+ return nil, err
+ }
+ p[k] = val
+ }
+ case []interface{}:
+ for i, v := range p {
+ val, err := evalProperties(context, v)
+ if err != nil {
+ return nil, err
+ }
+ p[i] = val
+ }
+ case string:
+ var js interface{}
+ err := json.Unmarshal([]byte(p), &js)
+ if err == nil {
+ modified, err := enumerateProperties(js, context)
+ if err != nil {
+ return nil, err
+ }
+ jsBytes, err := json.Marshal(modified)
+ if err != nil {
+ return nil, err
+ }
+ return string(jsBytes), nil
+ }
+ parser := NewParser(p)
+ val, err := parser.Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ properties = val
+ }
+ return properties, nil
+}
+
+func enumerateProperties(js interface{}, context utils.EvaluationContext) (interface{}, error) {
+ switch v := js.(type) {
+ case map[string]interface{}:
+ for key, val := range v {
+ if strVal, ok := val.(string); ok {
+ parser := NewParser(strVal)
+ val, err := parser.Eval(context)
+ if err != nil {
+ return nil, err
+ }
+ v[key] = val
+ } else {
+ nestedProps, err := enumerateProperties(val, context)
+ if err != nil {
+ return nil, err
+ }
+ v[key] = nestedProps
+ }
+ }
+ case []interface{}:
+ for i, val := range v {
+ nestedProps, err := enumerateProperties(val, context)
+ if err != nil {
+ return nil, err
+ }
+ v[i] = nestedProps
+ }
+ }
+ return js, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package utils
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+
+ coa_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/utils"
+)
+
+type Rule struct {
+ Type string `json:"type,omitempty"`
+ Required bool `json:"required,omitempty"`
+ Pattern string `json:"pattern,omitempty"`
+ Expression string `json:"expression,omitempty"`
+}
+type Schema struct {
+ Rules map[string]Rule `json:"rules,omitempty"`
+}
+
+type RuleResult struct {
+ Valid bool `json:"valid"`
+ Error string `json:"error,omitempty"`
+}
+
+type SchemaResult struct {
+ Valid bool `json:"valid"`
+ Errors map[string]RuleResult `json:"errors,omitempty"`
+}
+
+func (s *Schema) CheckProperties(properties map[string]interface{}, evaluationContext *coa_utils.EvaluationContext) (SchemaResult, error) {
+ context := evaluationContext
+ if context == nil {
+ context = &coa_utils.EvaluationContext{}
+ }
+ ret := SchemaResult{Valid: true, Errors: make(map[string]RuleResult)}
+ for k, v := range s.Rules {
+ if v.Type != "" {
+ if val, ok := properties[k]; ok {
+ if v.Type == "int" {
+ if _, err := strconv.Atoi(val.(string)); err != nil {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "property is not an int"}
+ }
+ } else if v.Type == "float" {
+ if _, err := strconv.ParseFloat(val.(string), 64); err != nil {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "property is not a float"}
+ }
+ } else if v.Type == "bool" {
+ if _, err := strconv.ParseBool(val.(string)); err != nil {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "property is not a bool"}
+ }
+ } else if v.Type == "uint" {
+ if _, err := strconv.ParseUint(val.(string), 10, 64); err != nil {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "property is not a uint"}
+ }
+ } else if v.Type == "string" {
+ // Do nothing
+ } else {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "unknown type"}
+ }
+ }
+ }
+ if v.Required {
+ if _, ok := properties[k]; !ok {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "missing required property"}
+ }
+ }
+ if v.Pattern != "" {
+ if val, ok := properties[k]; ok {
+ match, err := s.matchPattern(val.(string), v.Pattern)
+ if err != nil {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "error matching pattern: " + err.Error()}
+ }
+ if !match {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: fmt.Sprintf("property does not match pattern: %s", v.Pattern)}
+ }
+ }
+ }
+ if v.Expression != "" {
+ if val, ok := properties[k]; ok {
+ context.Value = val
+ parser := NewParser(v.Expression)
+ res, err := parser.Eval(*context)
+ if err != nil {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "error evaluating expression: " + err.Error()}
+ }
+ if res != "true" && res != "false" && res != true && res != false {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: "expression does not evaluate to boolean"}
+ }
+ if res != "true" && res != true {
+ ret.Valid = false
+ ret.Errors[k] = RuleResult{Valid: false, Error: fmt.Sprintf("property does not match expression: %s", v.Expression)}
+ }
+ }
+ }
+ }
+ return ret, nil
+}
+func (s *Schema) matchPattern(value string, pattern string) (bool, error) {
+ regexPattern := pattern
+ switch pattern {
+ case "<email>":
+ regexPattern = `^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$`
+ case "<url>":
+ regexPattern = `^https?://.*$`
+ case "<uuid>":
+ regexPattern = `^[a-f\d]{8}(-[a-f\d]{4}){4}[a-f\d]{8}$`
+ case "<dns-label>":
+ regexPattern = `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`
+ case "<dns-name>":
+ regexPattern = `^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)+[a-z]{2,}$`
+ case "<ip4>":
+ regexPattern = `^(\d{1,3}\.){3}\d{1,3}$`
+ case "<ip4-range>":
+ regexPattern = `^(\d{1,3}\.){3}\d{1,3}-(\d{1,3}\.){3}\d{1,3}$`
+ case "<port>":
+ regexPattern = `^\d{1,5}$`
+ case "<mac-address>":
+ regexPattern = `^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$`
+ case "<cidr>":
+ regexPattern = `^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$`
+ case "<ip6>":
+ regexPattern = `^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$`
+ case "<ip6-range>":
+ regexPattern = `^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}-([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$`
+ }
+ matched, err := regexp.MatchString(regexPattern, value)
+ if err != nil {
+ return false, err
+ }
+ return matched, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package utils
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "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/observability"
+ observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+)
+
+const (
+ SymphonyAPIAddressBase = "http://symphony-service:8080/v1alpha2/"
+)
+
+type authRequest struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+}
+type authResponse struct {
+ AccessToken string `json:"accessToken"`
+ TokenType string `json:"tokenType"`
+ Username string `json:"username"`
+ Roles []string `json:"roles"`
+}
+
+// We shouldn't use specific error types
+// SummarySpecError represents an error that includes a SummarySpec in its message
+// field.
+// type SummarySpecError struct {
+// Code string `json:"code"`
+// Message string `json:"message"`
+// }
+
+// func (e *SummarySpecError) Error() string {
+// return fmt.Sprintf(
+// "failed to invoke Symphony API: [%s] - %s",
+// e.Code,
+// e.Message,
+// )
+// }
+
+var log = logger.NewLogger("coa.runtime")
+
+func GetInstancesForAllScope(context context.Context, baseUrl string, user string, password string) ([]model.InstanceState, error) {
+ ret := make([]model.InstanceState, 0)
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+
+ response, err := callRestAPI(context, baseUrl, "instances", "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+
+func GetInstances(context context.Context, baseUrl string, user string, password string, scope string) ([]model.InstanceState, error) {
+ ret := make([]model.InstanceState, 0)
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+ path := "instances?scope=" + scope
+ response, err := callRestAPI(context, baseUrl, path, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+func GetSites(context context.Context, baseUrl string, user string, password string) ([]model.SiteState, error) {
+ ret := make([]model.SiteState, 0)
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+
+ response, err := callRestAPI(context, baseUrl, "federation/registry", "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+
+ return ret, nil
+}
+func SyncActivationStatus(context context.Context, baseUrl string, user string, password string, status model.ActivationStatus) error {
+ token, err := auth(context, baseUrl, user, password)
+
+ if err != nil {
+ return err
+ }
+ jData, _ := json.Marshal(status)
+ _, err = callRestAPI(context, baseUrl, "federation/sync", "POST", jData, token)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+func GetCatalogs(context context.Context, baseUrl string, user string, password string) ([]model.CatalogState, error) {
+ ret := make([]model.CatalogState, 0)
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+
+ response, err := callRestAPI(context, baseUrl, "catalogs/registry", "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+
+ return ret, nil
+}
+func GetCatalog(context context.Context, baseUrl string, catalog string, user string, password string) (model.CatalogState, error) {
+ ret := model.CatalogState{}
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+
+ catalogName := catalog
+ if strings.HasPrefix(catalogName, "<") && strings.HasSuffix(catalogName, ">") {
+ catalogName = catalogName[1 : len(catalogName)-1]
+ }
+
+ response, err := callRestAPI(context, baseUrl, "catalogs/registry/"+catalogName, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+func GetCampaign(context context.Context, baseUrl string, campaign string, user string, password string) (model.CampaignState, error) {
+ ret := model.CampaignState{}
+ token, err := auth(context, baseUrl, user, password)
+
+ if err != nil {
+ return ret, err
+ }
+
+ response, err := callRestAPI(context, baseUrl, "campaigns/"+campaign, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+func PublishActivationEvent(context context.Context, baseUrl string, user string, password string, event v1alpha2.ActivationData) error {
+ token, err := auth(context, baseUrl, user, password)
+
+ if err != nil {
+ return err
+ }
+ jData, _ := json.Marshal(event)
+ _, err = callRestAPI(context, baseUrl, "jobs", "POST", jData, token)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+func GetABatchForSite(context context.Context, baseUrl string, site string, user string, password string) (model.SyncPackage, error) {
+ ret := model.SyncPackage{}
+ token, err := auth(context, baseUrl, user, password)
+
+ if err != nil {
+ return ret, err
+ }
+
+ response, err := callRestAPI(context, baseUrl, "federation/sync/"+site+"?count=10", "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+func GetActivation(context context.Context, baseUrl string, activation string, user string, password string) (model.ActivationState, error) {
+ ret := model.ActivationState{}
+ token, err := auth(context, baseUrl, user, password)
+
+ if err != nil {
+ return ret, err
+ }
+
+ response, err := callRestAPI(context, baseUrl, "activations/registry/"+activation, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+func ReportActivationStatus(context context.Context, baseUrl string, name string, user string, password string, activation model.ActivationStatus) error {
+ token, err := auth(context, baseUrl, user, password)
+
+ if err != nil {
+ return err
+ }
+
+ jData, _ := json.Marshal(activation)
+ _, err = callRestAPI(context, baseUrl, "activations/status/"+name, "POST", jData, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+func GetInstance(context context.Context, baseUrl string, instance string, user string, password string, scope string) (model.InstanceState, error) {
+ ret := model.InstanceState{}
+ token, err := auth(context, baseUrl, user, password)
+
+ if err != nil {
+ return ret, err
+ }
+
+ path := "instances/" + instance
+ path = path + "?scope=" + scope
+ response, err := callRestAPI(context, baseUrl, path, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+func UpsertCatalog(context context.Context, baseUrl string, catalog string, user string, password string, payload []byte) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+
+ _, err = callRestAPI(context, baseUrl, "catalogs/registry/"+catalog, "POST", payload, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func CreateInstance(context context.Context, baseUrl string, instance string, user string, password string, payload []byte, scope string) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+
+ path := "instances/" + instance
+ path = path + "?scope=" + scope
+ _, err = callRestAPI(context, baseUrl, path, "POST", payload, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func DeleteCatalog(context context.Context, baseUrl string, catalog string, user string, password string) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+
+ _, err = callRestAPI(context, baseUrl, "catalogs/registry/"+catalog, "DELETE", nil, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func DeleteInstance(context context.Context, baseUrl string, instance string, user string, password string, scope string) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+ path := "instances/" + instance
+ path = path + "?direct=true&scope=" + scope
+ _, err = callRestAPI(context, baseUrl, path, "DELETE", nil, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func DeleteTarget(context context.Context, baseUrl string, target string, user string, password string, scope string) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+ path := "targets/registry/" + target
+ path = path + "?direct=true&scope=" + scope
+ _, err = callRestAPI(context, baseUrl, path, "DELETE", nil, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func GetSolutionsForAllScope(context context.Context, baseUrl string, user string, password string) ([]model.SolutionState, error) {
+ ret := make([]model.SolutionState, 0)
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+
+ response, err := callRestAPI(context, baseUrl, "solutions", "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+
+func GetSolutions(context context.Context, baseUrl string, user string, password string, scope string) ([]model.SolutionState, error) {
+ ret := make([]model.SolutionState, 0)
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+ path := "solution" + "?scope=" + scope
+ response, err := callRestAPI(context, baseUrl, path, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+
+func GetSolution(context context.Context, baseUrl string, solution string, user string, password string, scope string) (model.SolutionState, error) {
+ ret := model.SolutionState{}
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+ path := "solutions/" + solution
+ path = path + "?scope=" + scope
+ response, err := callRestAPI(context, baseUrl, path, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+
+func UpsertTarget(context context.Context, baseUrl string, solution string, user string, password string, payload []byte, scope string) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+ path := "targets/registry/" + solution
+ path = path + "?scope=" + scope
+ _, err = callRestAPI(context, baseUrl, path, "POST", payload, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func UpsertSolution(context context.Context, baseUrl string, solution string, user string, password string, payload []byte, scope string) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+ path := "solutions/" + solution
+ path = path + "?scope=" + scope
+ _, err = callRestAPI(context, baseUrl, path, "POST", payload, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func DeleteSolution(context context.Context, baseUrl string, solution string, user string, password string, scope string) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+ path := "solutions/" + solution
+ path = path + "?scope=" + scope
+ _, err = callRestAPI(context, baseUrl, path, "DELETE", nil, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func GetTarget(context context.Context, baseUrl string, target string, user string, password string, scope string) (model.TargetState, error) {
+ ret := model.TargetState{}
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+ path := "targets/registry/" + target
+ path = path + "?scope=" + scope
+ response, err := callRestAPI(context, baseUrl, path, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+
+func GetTargetsForAllScope(context context.Context, baseUrl string, user string, password string) ([]model.TargetState, error) {
+ ret := []model.TargetState{}
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+
+ response, err := callRestAPI(context, baseUrl, "targets/registry", "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+
+func GetTargets(context context.Context, baseUrl string, user string, password string, scope string) ([]model.TargetState, error) {
+ ret := []model.TargetState{}
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return ret, err
+ }
+ path := "targets/registry"
+ path = path + "?scope=" + scope
+ response, err := callRestAPI(context, baseUrl, path, "GET", nil, token)
+ if err != nil {
+ return ret, err
+ }
+
+ err = json.Unmarshal(response, &ret)
+ if err != nil {
+ return ret, err
+ }
+ return ret, nil
+}
+
+func UpdateSite(context context.Context, baseUrl string, site string, user string, password string, payload []byte) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+
+ _, err = callRestAPI(context, baseUrl, "federation/status/"+site, "POST", payload, token)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func CreateTarget(context context.Context, baseUrl string, target string, user string, password string, payload []byte, scope string) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+ path := "targets/registry/" + target
+ path = path + "?scope=" + scope
+ _, err = callRestAPI(context, baseUrl, path, "POST", payload, token)
+ if err != nil {
+ return err
+ }
+ log.Info(">>>>>CreateTarget Succeed: " + target + " " + scope)
+ return nil
+}
+
+func MatchTargets(instance model.InstanceState, targets []model.TargetState) []model.TargetState {
+ ret := make(map[string]model.TargetState)
+ if instance.Spec.Target.Name != "" {
+ for _, t := range targets {
+ if matchString(instance.Spec.Target.Name, t.Id) {
+ ret[t.Id] = t
+ }
+ }
+ }
+
+ if len(instance.Spec.Target.Selector) > 0 {
+ for _, t := range targets {
+ fullMatch := true
+ for k, v := range instance.Spec.Target.Selector {
+ if tv, ok := t.Spec.Properties[k]; !ok || !matchString(v, tv) {
+ fullMatch = false
+ }
+ }
+
+ if fullMatch {
+ ret[t.Id] = t
+ }
+ }
+ }
+
+ slice := make([]model.TargetState, 0, len(ret))
+ for _, v := range ret {
+ slice = append(slice, v)
+ }
+
+ return slice
+}
+
+func CreateSymphonyDeploymentFromTarget(target model.TargetState) (model.DeploymentSpec, error) {
+ key := fmt.Sprintf("%s-%s", "target-runtime", target.Id)
+ scope := target.Spec.Scope
+
+ ret := model.DeploymentSpec{}
+ solution := model.SolutionSpec{
+ DisplayName: key,
+ Scope: scope,
+ Components: make([]model.ComponentSpec, 0),
+ Metadata: make(map[string]string, 0),
+ }
+ for k, v := range target.Spec.Metadata {
+ solution.Metadata[k] = v
+ }
+
+ for _, component := range target.Spec.Components {
+ var c model.ComponentSpec
+ data, _ := json.Marshal(component)
+ err := json.Unmarshal(data, &c)
+
+ if err != nil {
+ return ret, err
+ }
+ solution.Components = append(solution.Components, c)
+ }
+
+ targets := make(map[string]model.TargetSpec)
+ var t model.TargetSpec
+ data, _ := json.Marshal(target.Spec)
+ err := json.Unmarshal(data, &t)
+ if err != nil {
+ return ret, err
+ }
+
+ targets[target.Id] = t
+
+ instance := model.InstanceSpec{
+ Name: key,
+ DisplayName: key,
+ Scope: scope,
+ Solution: key,
+ Target: model.TargetSelector{
+ Name: target.Id,
+ },
+ }
+
+ ret.Solution = solution
+ ret.Instance = instance
+ ret.Targets = targets
+ ret.SolutionName = key
+ assignments, err := AssignComponentsToTargets(ret.Solution.Components, ret.Targets)
+ if err != nil {
+ return ret, err
+ }
+
+ ret.Assignments = make(map[string]string)
+ for k, v := range assignments {
+ ret.Assignments[k] = v
+ }
+
+ return ret, nil
+}
+
+func CreateSymphonyDeployment(instance model.InstanceState, solution model.SolutionState, targets []model.TargetState, devices []model.DeviceState) (model.DeploymentSpec, error) {
+ ret := model.DeploymentSpec{}
+ ret.Generation = instance.Spec.Generation
+ // convert instance
+ sInstance := instance.Spec
+
+ sInstance.Name = instance.Id
+ sInstance.Scope = instance.Spec.Scope
+
+ // convert solution
+ sSolution := solution.Spec
+
+ sSolution.DisplayName = solution.Spec.DisplayName
+ sSolution.Scope = solution.Spec.Scope
+
+ // convert targets
+ sTargets := make(map[string]model.TargetSpec)
+ for _, t := range targets {
+ sTargets[t.Id] = *t.Spec
+ }
+
+ //TODO: handle devices
+ ret.Solution = *sSolution
+ ret.Targets = sTargets
+ ret.Instance = *sInstance
+ ret.SolutionName = solution.Id
+
+ assignments, err := AssignComponentsToTargets(ret.Solution.Components, ret.Targets)
+ if err != nil {
+ return ret, err
+ }
+
+ ret.Assignments = make(map[string]string)
+ for k, v := range assignments {
+ ret.Assignments[k] = v
+ }
+
+ return ret, nil
+}
+
+func AssignComponentsToTargets(components []model.ComponentSpec, targets map[string]model.TargetSpec) (map[string]string, error) {
+ //TODO: evaluate constraints
+ ret := make(map[string]string)
+ for key, target := range targets {
+ ret[key] = ""
+ for _, component := range components {
+ match := true
+ if component.Constraints != "" {
+ parser := NewParser(component.Constraints)
+ val, err := parser.Eval(utils.EvaluationContext{Properties: target.Properties})
+ if err != nil {
+ return ret, err
+ }
+ match = (val == "true" || val == true)
+ }
+ if match {
+ ret[key] += "{" + component.Name + "}"
+ }
+ }
+ }
+
+ return ret, nil
+}
+func GetSummary(context context.Context, baseUrl string, user string, password string, id string, scope string) (model.SummaryResult, error) {
+ result := model.SummaryResult{}
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return result, err
+ }
+ path := "solution/queue"
+ path = path + "?instance=" + id + "&scope=" + scope
+ ret, err := callRestAPI(context, baseUrl, path, "GET", nil, token) // TODO: We can pass empty token now because is path is a "back-door", as it was designed to be invoked from a trusted environment, which should be also protected with auth
+ if err != nil {
+ return result, err
+ }
+ if ret != nil {
+ err = json.Unmarshal(ret, &result)
+ if err != nil {
+ return result, err
+ }
+ }
+ return result, nil
+}
+func CatalogHook(context context.Context, baseUrl string, user string, password string, payload []byte) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+ path := "federation/k8shook?objectType=catalog"
+ _, err = callRestAPI(context, baseUrl, path, "POST", payload, token)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func QueueJob(context context.Context, baseUrl string, user string, password string, id string, scope string, isDelete bool, isTarget bool) error {
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return err
+ }
+ path := "solution/queue?instance=" + id
+ if isDelete {
+ path += "&delete=true"
+ }
+ if isTarget {
+ path += "&target=true"
+ }
+ path = path + "&scope=" + scope
+ _, err = callRestAPI(context, baseUrl, path, "POST", nil, token) // TODO: We can pass empty token now because is path is a "back-door", as it was designed to be invoked from a trusted environment, which should be also protected with auth
+ if err != nil {
+ return err
+ }
+ return nil
+}
+func Reconcile(context context.Context, baseUrl string, user string, password string, deployment model.DeploymentSpec, scope string, isDelete bool) (model.SummarySpec, error) {
+ summary := model.SummarySpec{}
+ payload, _ := json.Marshal(deployment)
+
+ path := "solution/reconcile" + "?scope=" + scope
+ if isDelete {
+ path = path + "&delete=true"
+ }
+ token, err := auth(context, baseUrl, user, password)
+ if err != nil {
+ return summary, err
+ }
+ ret, err := callRestAPI(context, baseUrl, path, "POST", payload, token) // TODO: We can pass empty token now because is path is a "back-door", as it was designed to be invoked from a trusted environment, which should be also protected with auth
+ if err != nil {
+ return summary, err
+ }
+ if ret != nil {
+ err = json.Unmarshal(ret, &summary)
+ if err != nil {
+ return summary, err
+ }
+ }
+ return summary, nil
+}
+func auth(context context.Context, baseUrl string, user string, password string) (string, error) {
+ request := authRequest{Username: user, Password: password}
+ requestData, _ := json.Marshal(request)
+ ret, err := callRestAPI(context, baseUrl, "users/auth", "POST", requestData, "")
+ if err != nil {
+ return "", err
+ }
+
+ var response authResponse
+ err = json.Unmarshal(ret, &response)
+ if err != nil {
+ return "", err
+ }
+
+ return response.AccessToken, nil
+}
+func callRestAPI(context context.Context, baseUrl string, route string, method string, payload []byte, token string) ([]byte, error) {
+ context, span := observability.StartSpan("Symphony-API-Client", context, &map[string]string{
+ "method": "callRestAPI",
+ "http.method": method,
+ "http.url": baseUrl + route,
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ log.Infof("Calling Symphony API: %s %s, spanId: %s, traceId: %s", method, baseUrl+route, span.SpanContext().SpanID().String(), span.SpanContext().TraceID().String())
+
+ client := &http.Client{}
+ rUrl := baseUrl + route
+ var req *http.Request
+ if payload != nil {
+ req, err = http.NewRequestWithContext(context, method, rUrl, bytes.NewBuffer(payload))
+ observ_utils.PropagateSpanContextToHttpRequestHeader(req)
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+ } else {
+ req, err = http.NewRequestWithContext(context, method, rUrl, nil)
+ observ_utils.PropagateSpanContextToHttpRequestHeader(req)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ if token != "" {
+ req.Header.Set("Authorization", "Bearer "+token)
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ bodyBytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode >= 300 {
+ // TODO: Can we remove the following? It doesn't seem right.
+ // I'm afraid some downstream logic is expecting this behavior, though.
+ // if resp.StatusCode == 404 { // API service is already gone
+ // return nil, nil
+ // }
+ err = v1alpha2.FromHTTPResponseCode(resp.StatusCode, bodyBytes)
+ return nil, err
+ }
+ err = nil
+ log.Infof("Symphony API succeeded: %s %s, spanId: %s, traceId: %s", method, baseUrl+route, span.SpanContext().SpanID().String(), span.SpanContext().TraceID().String())
+
+ return bodyBytes, nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package utils
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
+ oJsonpath "github.com/oliveagle/jsonpath"
+ "k8s.io/client-go/util/jsonpath"
+ "sigs.k8s.io/yaml"
+)
+
+const (
+ Must = "must"
+ Prefer = "prefer"
+ Reject = "reject"
+ Any = "any"
+)
+
+func matchString(src string, target string) bool {
+ if strings.Contains(src, "*") || strings.Contains(src, "%") {
+ p := strings.ReplaceAll(src, "*", ".*")
+ p = strings.ReplaceAll(p, "%", ".")
+ re := regexp.MustCompile(p)
+ return re.MatchString(target)
+ } else {
+ return src == target
+ }
+}
+
+func ReadInt32(col map[string]string, key string, defaultVal int32) int32 {
+ if v, ok := col[key]; ok {
+ i, e := ParseValue(v)
+ if e != nil {
+ return defaultVal
+ }
+ return i.(int32)
+ }
+ return defaultVal
+}
+func GetString(col map[string]string, key string) (string, error) {
+ if v, ok := col[key]; ok {
+ i, e := ParseValue(v)
+ if e != nil {
+ return "", e
+ }
+ return i.(string), nil
+ }
+ return "", fmt.Errorf("key %s is not found", key)
+}
+
+func ReadStringFromMapCompat(col map[string]interface{}, key string, defaultVal string) string {
+ if v, ok := col[key]; ok {
+ i, e := ParseValue(fmt.Sprintf("%v", v))
+ if e != nil {
+ return defaultVal
+ }
+ return i.(string)
+ }
+ return defaultVal
+}
+
+func ReadString(col map[string]string, key string, defaultVal string) string {
+ if v, ok := col[key]; ok {
+ i, e := ParseValue(v)
+ if e != nil {
+ return defaultVal
+ }
+ return i.(string)
+ }
+ return defaultVal
+}
+func ReadStringWithOverrides(col1 map[string]string, col2 map[string]string, key string, defaultVal string) string {
+ val := ReadString(col1, key, defaultVal)
+ return ReadString(col2, key, val)
+}
+func MergeCollection(col1 map[string]string, col2 map[string]string) map[string]string {
+ ret := make(map[string]string)
+ for k, v := range col1 {
+ ret[k] = v
+ }
+ for k, v := range col2 {
+ ret[k] = v
+ }
+ return ret
+}
+func CollectStringMap(col map[string]string, prefix string) map[string]string {
+ ret := make(map[string]string)
+ for k := range col {
+ if strings.HasPrefix(k, prefix) {
+ ret[k] = ReadString(col, k, "")
+ }
+ }
+ return ret
+}
+
+// TODO: we should get rid of this
+func ParseValue(v string) (interface{}, error) { //TODO: make this a generic utiliy
+ if v == "$true" {
+ return true, nil
+ } else if v == "$false" {
+ return false, nil
+ } else if strings.HasPrefix(v, "#") {
+ ri, e := strconv.Atoi(v[1:])
+ return int32(ri), e
+ } else if strings.HasPrefix(v, "{") && strings.HasSuffix(v, "}") {
+ var objmap map[string]*json.RawMessage
+ e := json.Unmarshal([]byte(v), &objmap)
+ return objmap, e
+ } else if strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
+ var objmap []map[string]*json.RawMessage
+ e := json.Unmarshal([]byte(v), &objmap)
+ return objmap, e
+ } else if strings.HasPrefix(v, "$") {
+ return os.Getenv(v[1:]), nil
+ }
+ return v, nil
+}
+
+// TODO: This should not be used anymore
+func ProjectValue(val string, name string) string {
+ if strings.Contains(val, "${{$instance()}}") {
+ val = strings.ReplaceAll(val, "${{$instance()}}", name)
+ }
+ return val
+}
+
+func FormatObject(obj interface{}, isArray bool, path string, format string) ([]byte, error) {
+ jData, _ := json.Marshal(obj)
+ if path == "" && format == "" {
+ return jData, nil
+ }
+ var dict interface{}
+ if isArray {
+ dict = make([]map[string]interface{}, 0)
+ } else {
+ dict = make(map[string]interface{})
+ }
+ json.Unmarshal(jData, &dict)
+ if path != "" {
+ if path == "first_embedded" {
+ path = "$.spec.components[0].properties.embedded"
+ }
+ if isArray {
+ if format == "yaml" {
+ ret := make([]byte, 0)
+ for i, item := range dict.([]interface{}) {
+ ob, _ := oJsonpath.JsonPathLookup(item, path)
+ if s, ok := ob.(string); ok {
+ str, err := strconv.Unquote(strings.TrimSpace(s))
+ if err != nil {
+ str = strings.TrimSpace(s)
+ }
+ var o interface{}
+ err = yaml.Unmarshal([]byte(str), &o)
+ if err != nil {
+ jData = []byte(s)
+ } else {
+ jData, _ = yaml.Marshal(o)
+ }
+ } else {
+ jData, _ = yaml.Marshal(ob)
+ }
+ if i > 0 {
+ ret = append(ret, []byte("---\n")...)
+ }
+ ret = append(ret, jData...)
+ }
+ jData = ret
+ } else {
+ ret := make([]interface{}, 0)
+ for _, item := range dict.([]interface{}) {
+ ob, _ := oJsonpath.JsonPathLookup(item, path)
+ ret = append(ret, ob)
+ jData, _ = yaml.Marshal(ob)
+ }
+ jData, _ = json.Marshal(ret)
+ }
+ } else {
+ ob, _ := oJsonpath.JsonPathLookup(dict, path)
+ if format == "yaml" {
+ if s, ok := ob.(string); ok {
+ str, err := strconv.Unquote(strings.TrimSpace(s))
+ if err != nil {
+ str = strings.TrimSpace(s)
+ }
+ var o interface{}
+ err = yaml.Unmarshal([]byte(str), &o)
+ if err != nil {
+ jData = []byte(str)
+ } else {
+ jData, _ = yaml.Marshal(o)
+ }
+ } else {
+ jData, _ = yaml.Marshal(ob)
+ }
+ } else {
+ jData, _ = json.Marshal(ob)
+ }
+ }
+ }
+ return jData, nil
+}
+
+func toInterfaceMap(m map[string]string) map[string]interface{} {
+ ret := make(map[string]interface{})
+ for k, v := range m {
+ ret[k] = v
+ }
+ return ret
+}
+func FormatAsString(val interface{}) string {
+ switch tv := val.(type) {
+ case string:
+ return tv
+ case int:
+ return strconv.Itoa(tv)
+ case int32:
+ return strconv.Itoa(int(tv))
+ case int64:
+ return strconv.Itoa(int(tv))
+ case float32:
+ return strconv.FormatFloat(float64(tv), 'f', -1, 32)
+ case float64:
+ return strconv.FormatFloat(tv, 'f', -1, 64)
+ case bool:
+ return strconv.FormatBool(tv)
+ case map[string]interface{}:
+ ret, _ := json.Marshal(tv)
+ return string(ret)
+ case []interface{}:
+ ret, _ := json.Marshal(tv)
+ return string(ret)
+ default:
+ return fmt.Sprintf("%v", tv)
+ }
+}
+func JsonPathQuery(obj interface{}, jsonPath string) (interface{}, error) {
+ jPath := jsonPath
+ if !strings.HasPrefix(jPath, "{") {
+ jPath = "{" + jsonPath + "}" // k8s.io/client-go/util/jsonpath requires JsonPath expression to be wrapped in {}
+ }
+
+ result, err := jsonPathQuery(obj, jPath)
+ if err == nil {
+ return result, nil
+ }
+
+ // This is a workaround for filtering by root-level attributes. In this case, we need to
+ // wrap the object into an array and then query the array.
+ var arr []interface{}
+ switch obj.(type) {
+ case []interface{}:
+ // the object is already an array, so the query didn't work
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("no matches found by JsonPath query '%s'", jsonPath), v1alpha2.InternalError)
+ default:
+ arr = append(arr, obj)
+ }
+ return jsonPathQuery(arr, jPath)
+}
+func jsonPathQuery(obj interface{}, jsonPath string) (interface{}, error) {
+ jpLookup := jsonpath.New("lookup")
+ jpLookup.AllowMissingKeys(true)
+ jpLookup.EnableJSONOutput(true)
+
+ err := jpLookup.Parse(jsonPath)
+ if err != nil {
+ return nil, err
+ }
+
+ var buf bytes.Buffer
+ err = jpLookup.Execute(&buf, obj)
+ if err != nil {
+ return nil, err
+ }
+
+ var result []interface{}
+ err = json.Unmarshal(buf.Bytes(), &result)
+
+ if err != nil {
+ return nil, err
+ } else if len(result) == 0 {
+ return nil, v1alpha2.NewCOAError(nil, fmt.Sprintf("no matches found by JsonPath query '%s'", jsonPath), v1alpha2.InternalError)
+ } else if len(result) == 1 {
+ return result[0], nil
+ } else {
+ return result, nil
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/activations"
+ "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 vLog = logger.NewLogger("coa.runtime")
+
+type ActivationsVendor struct {
+ vendors.Vendor
+ ActivationsManager *activations.ActivationsManager
+}
+
+func (o *ActivationsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Activations",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *ActivationsVendor) 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.(*activations.ActivationsManager); ok {
+ e.ActivationsManager = c
+ }
+ }
+ if e.ActivationsManager == nil {
+ return v1alpha2.NewCOAError(nil, "activations manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *ActivationsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "activations"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route + "/registry",
+ Version: o.Version,
+ Handler: o.onActivations,
+ Parameters: []string{"name?"},
+ },
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/status",
+ Version: o.Version,
+ Handler: o.onStatus,
+ Parameters: []string{"name?"},
+ },
+ }
+}
+
+func (c *ActivationsVendor) onStatus(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Activations Vendor", request.Context, &map[string]string{
+ "method": "onStatus",
+ })
+ defer span.End()
+
+ cLog.Info("V (Activations Vendor): onStatus")
+ switch request.Method {
+ case fasthttp.MethodPost:
+ ctx, span := observability.StartSpan("onStatus-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+ var status model.ActivationStatus
+ err := json.Unmarshal(request.Body, &status)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ err = c.ActivationsManager.ReportStatus(ctx, id, status)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+func (c *ActivationsVendor) onActivations(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Activations Vendor", request.Context, &map[string]string{
+ "method": "onActivations",
+ })
+ defer span.End()
+
+ cLog.Info("V (Activations Vendor): onActivations")
+
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onActivations-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ state, err = c.ActivationsManager.ListSpec(ctx)
+ isArray = true
+ } else {
+ state, err = c.ActivationsManager.GetSpec(ctx, id)
+ }
+ if err != nil {
+ 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("onActivations-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+
+ var activation model.ActivationSpec
+
+ err := json.Unmarshal(request.Body, &activation)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+
+ err = c.ActivationsManager.UpsertSpec(ctx, id, activation)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ entry, err := c.ActivationsManager.GetSpec(ctx, id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ c.Context.Publish("activation", v1alpha2.Event{
+ Body: v1alpha2.ActivationData{
+ Campaign: activation.Campaign,
+ ActivationGeneration: entry.Spec.Generation,
+ Activation: id,
+ Stage: "",
+ Inputs: activation.Inputs,
+ },
+ })
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ case fasthttp.MethodDelete:
+ ctx, span := observability.StartSpan("onActivations-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ err := c.ActivationsManager.DeleteSpec(ctx, id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "context"
+ "encoding/json"
+ "strconv"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/reference"
+ "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/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 log = logger.NewLogger("coa.runtime")
+
+type AgentVendor struct {
+ vendors.Vendor
+ ReferenceManager *reference.ReferenceManager
+}
+
+func (o *AgentVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Agent",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *AgentVendor) 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.(*reference.ReferenceManager); ok {
+ e.ReferenceManager = c
+ }
+ }
+ if e.ReferenceManager == nil {
+ return v1alpha2.NewCOAError(nil, "reference manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *AgentVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "agent"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost},
+ Route: route + "/references",
+ Version: o.Version,
+ Handler: o.onReference,
+ },
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/config",
+ Version: o.Version,
+ Handler: o.onConfig,
+ },
+ }
+}
+func (c *AgentVendor) onConfig(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Agent Vendor", request.Context, &map[string]string{
+ "method": "onConfig",
+ })
+ defer span.End()
+
+ log.Infof("V (Agent): onConfig %s", request.Method)
+
+ switch request.Method {
+ case fasthttp.MethodPost:
+ ctx, span := observability.StartSpan("Apply Config", pCtx, nil)
+ response := c.doApplyConfig(ctx, request.Parameters, request.Body)
+ return observ_utils.CloseSpanWithCOAResponse(span, response)
+ }
+
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+func (c *AgentVendor) onReference(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Agent Vendor", request.Context, &map[string]string{
+ "method": "onReference",
+ })
+ defer span.End()
+
+ log.Infof("V (Agent): onReference %s", request.Method)
+
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("Get References", pCtx, nil)
+ response := c.doGet(ctx, request.Parameters)
+ return observ_utils.CloseSpanWithCOAResponse(span, response)
+ case fasthttp.MethodPost:
+ ctx, span := observability.StartSpan("Report Status", pCtx, nil)
+ response := c.doPost(ctx, request.Parameters, request.Body)
+ return observ_utils.CloseSpanWithCOAResponse(span, response)
+ }
+
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+func (c *AgentVendor) doGet(ctx context.Context, parameters map[string]string) v1alpha2.COAResponse {
+ var scope = "default"
+ var kind = ""
+ var ref = ""
+ var group = ""
+ var id = ""
+ var version = ""
+ var fieldSelector = ""
+ var labelSelector = ""
+ var instance = ""
+ var lookup = ""
+ var platform = ""
+ var flavor = ""
+ var iteration = ""
+ var alias = ""
+ if v, ok := parameters["scope"]; ok {
+ scope = v
+ }
+ if v, ok := parameters["ref"]; ok {
+ ref = v
+ }
+ if v, ok := parameters["kind"]; ok {
+ kind = v
+ }
+ if v, ok := parameters["version"]; ok {
+ version = v
+ }
+ if v, ok := parameters["group"]; ok {
+ group = v
+ }
+ if v, ok := parameters["id"]; ok {
+ id = v
+ }
+ if v, ok := parameters["field-selector"]; ok {
+ fieldSelector = v
+ }
+ if v, ok := parameters["label-selector"]; ok {
+ labelSelector = v
+ }
+ if v, ok := parameters["instance"]; ok {
+ instance = v
+ }
+ if v, ok := parameters["platform"]; ok {
+ platform = v
+ }
+ if v, ok := parameters["flavor"]; ok {
+ flavor = v
+ }
+ if v, ok := parameters["lookup"]; ok {
+ lookup = v
+ }
+ if v, ok := parameters["iteration"]; ok {
+ iteration = v
+ }
+ if v, ok := parameters["alias"]; ok {
+ alias = v
+ }
+
+ var data []byte
+ var err error
+ if instance != "" {
+ data, err = c.ReferenceManager.GetExt(ref, scope, id, group, kind, version, instance, model.SolutionGroup, "instances", "v1", "", alias)
+ } else if lookup != "" {
+ data, err = c.ReferenceManager.GetExt(ref, scope, id, group, kind, version, instance, lookup, platform, flavor, iteration, "")
+ } else {
+ data, err = c.ReferenceManager.Get(ref, id, scope, group, kind, version, labelSelector, fieldSelector)
+ }
+ if err != nil {
+ return v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ }
+ }
+ return v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: data,
+ ContentType: "application/json",
+ }
+}
+
+func (c *AgentVendor) doApplyConfig(ctx context.Context, parameters map[string]string, data []byte) v1alpha2.COAResponse {
+ var config managers.ProviderConfig
+ err := json.Unmarshal(data, &config)
+ if err != nil {
+ return v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte(err.Error()),
+ }
+ }
+ // TODO: The following is temporary implementation. A proper mechanism to reconfigure providers/managers/vendors is needed. This doesn't handle scaling out
+ // (when multiple vendor instances are behind load balancer), either
+ switch config.Type {
+ case "providers.reference.customvision":
+ for _, p := range c.ReferenceManager.ReferenceProviders {
+ err = p.Reconfigure(config.Config)
+ if err != nil {
+ return v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ }
+ }
+ }
+ }
+ return v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: []byte("{}"),
+ ContentType: "application/json",
+ }
+}
+
+func (c *AgentVendor) doPost(ctx context.Context, parameters map[string]string, data []byte) v1alpha2.COAResponse {
+ var scope = "default"
+ var kind = ""
+ var group = ""
+ var id = ""
+ var version = ""
+ var overwrite = false
+ if v, ok := parameters["scope"]; ok {
+ scope = v
+ }
+ if v, ok := parameters["kind"]; ok {
+ kind = v
+ }
+ if v, ok := parameters["version"]; ok {
+ version = v
+ }
+ if v, ok := parameters["group"]; ok {
+ group = v
+ }
+ if v, ok := parameters["id"]; ok {
+ id = v
+ }
+ if v, ok := parameters["overwrite"]; ok {
+ overwrite, _ = strconv.ParseBool(v)
+ }
+ properties := make(map[string]string)
+ err := json.Unmarshal(data, &properties)
+ if err != nil {
+ return v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ }
+ }
+ err = c.ReferenceManager.Report(id, scope, group, kind, version, properties, overwrite)
+ if err != nil {
+ return v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ }
+ }
+ return v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: data,
+ ContentType: "application/json",
+ }
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/activations"
+ "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/providers"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors"
+)
+
+type BackgroundJobVendor struct {
+ vendors.Vendor
+ // Add a new manager if you want to add another background job
+ ActivationsCleanerManager *activations.ActivationsCleanupManager
+}
+
+func (s *BackgroundJobVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: s.Vendor.Version,
+ Name: "BackgroundJob",
+ Producer: "Microsoft",
+ }
+}
+
+func (o *BackgroundJobVendor) GetEndpoints() []v1alpha2.Endpoint {
+ return []v1alpha2.Endpoint{}
+}
+
+func (s *BackgroundJobVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error {
+ err := s.Vendor.Init(config, factories, providers, pubsubProvider)
+ if err != nil {
+ return err
+ }
+ for _, m := range s.Managers {
+ if c, ok := m.(*activations.ActivationsCleanupManager); ok {
+ s.ActivationsCleanerManager = c
+ }
+ // Load a new manager if you want to add another background job
+ }
+ if s.ActivationsCleanerManager != nil {
+ log.Info("ActivationsCleanupManager is enabled")
+ } else {
+ log.Info("ActivationsCleanupManager is disabled")
+ }
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/campaigns"
+ "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 cLog = logger.NewLogger("coa.runtime")
+
+type CampaignsVendor struct {
+ vendors.Vendor
+ CampaignsManager *campaigns.CampaignsManager
+}
+
+func (o *CampaignsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Campaigns",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *CampaignsVendor) 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.(*campaigns.CampaignsManager); ok {
+ e.CampaignsManager = c
+ }
+ }
+ if e.CampaignsManager == nil {
+ return v1alpha2.NewCOAError(nil, "campaigns manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *CampaignsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "campaigns"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onCampaigns,
+ Parameters: []string{"name?"},
+ },
+ }
+}
+
+func (c *CampaignsVendor) onCampaigns(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Campaigns Vendor", request.Context, &map[string]string{
+ "method": "onCampaigns",
+ })
+ defer span.End()
+ cLog.Info("V (Campaigns): onCampaigns")
+
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onCampaigns-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ state, err = c.CampaignsManager.ListSpec(ctx)
+ isArray = true
+ } else {
+ state, err = c.CampaignsManager.GetSpec(ctx, id)
+ }
+ if err != nil {
+ 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("onCampaigns-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+
+ var campaign model.CampaignSpec
+
+ err := json.Unmarshal(request.Body, &campaign)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+
+ err = c.CampaignsManager.UpsertSpec(ctx, id, campaign)
+ if err != nil {
+ 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("onCampaigns-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ err := c.CampaignsManager.DeleteSpec(ctx, id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/catalogs"
+ "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 lLog = logger.NewLogger("coa.runtime")
+
+type CatalogsVendor struct {
+ vendors.Vendor
+ CatalogsManager *catalogs.CatalogsManager
+}
+
+func (e *CatalogsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: e.Vendor.Version,
+ Name: "Catalogs",
+ Producer: "Microsoft",
+ }
+}
+func (e *CatalogsVendor) 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.(*catalogs.CatalogsManager); ok {
+ e.CatalogsManager = c
+ }
+ }
+ if e.CatalogsManager == nil {
+ return v1alpha2.NewCOAError(nil, "catalogs manager is not supplied", v1alpha2.MissingConfig)
+ }
+ e.Vendor.Context.Subscribe("catalog-sync", func(topic string, event v1alpha2.Event) error {
+ jData, _ := json.Marshal(event.Body)
+ var job v1alpha2.JobData
+ err := json.Unmarshal(jData, &job)
+ if err == nil {
+ var catalog model.CatalogSpec
+ jData, _ = json.Marshal(job.Body)
+ err = json.Unmarshal(jData, &catalog)
+ if err == nil {
+ name := fmt.Sprintf("%s-%s", catalog.SiteId, catalog.Name)
+ catalog.Name = name
+ if catalog.ParentName != "" {
+ catalog.ParentName = fmt.Sprintf("%s-%s", catalog.SiteId, catalog.ParentName)
+ }
+ err := e.CatalogsManager.UpsertSpec(context.TODO(), name, catalog)
+ if err != nil {
+ return v1alpha2.NewCOAError(err, "failed to upsert catalog", v1alpha2.InternalError)
+ }
+ } else {
+ iLog.Errorf("Failed to unmarshal job body: %v", err)
+ return err
+ }
+ } else {
+ iLog.Errorf("Failed to unmarshal job data: %v", err)
+ return err
+ }
+ return nil
+ })
+ return nil
+}
+func (e *CatalogsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "catalogs"
+ if e.Route != "" {
+ route = e.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route + "/registry",
+ Version: e.Version,
+ Handler: e.onCatalogs,
+ Parameters: []string{"name?"},
+ },
+ {
+ Methods: []string{fasthttp.MethodGet},
+ Route: route + "/graph",
+ Version: e.Version,
+ Handler: e.onCatalogsGraph,
+ },
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/check",
+ Version: e.Version,
+ Handler: e.onCheck,
+ },
+ }
+}
+func (e *CatalogsVendor) onCheck(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ rCtx, span := observability.StartSpan("Catalogs Vendor", request.Context, &map[string]string{
+ "method": "onCheck",
+ })
+ defer span.End()
+
+ lLog.Info("V (Catalogs Vendor): onCheck")
+ switch request.Method {
+ case fasthttp.MethodPost:
+ var campaign model.CatalogSpec
+
+ err := json.Unmarshal(request.Body, &campaign)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ res, err := e.CatalogsManager.ValidateSpec(rCtx, campaign)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ if !res.Valid {
+ jData, _ := utils.FormatObject(res.Errors, true, "", "")
+ resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: jData,
+ ContentType: "application/json",
+ })
+ return resp
+ }
+ resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ return resp
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+func (e *CatalogsVendor) onCatalogsGraph(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ rCtx, span := observability.StartSpan("Catalogs Vendor", request.Context, &map[string]string{
+ "method": "onCatalogsGraph",
+ })
+ defer span.End()
+
+ lLog.Info("V (Catalogs Vendor): onCatalogsGraph")
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onCatalogsGraph-GET", rCtx, nil)
+ template := request.Parameters["template"]
+ switch template {
+ case "config-chains":
+ chains, err := e.CatalogsManager.GetChains(ctx, "config")
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ jData, _ := utils.FormatObject(chains, true, "", "")
+ resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: jData,
+ ContentType: "application/json",
+ })
+ return resp
+ case "asset-trees":
+ trees, err := e.CatalogsManager.GetTrees(ctx, "asset")
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ jData, _ := utils.FormatObject(trees, true, "", "")
+ resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: jData,
+ ContentType: "application/json",
+ })
+ return resp
+ default:
+ resp := observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte("{\"result\": \"400 - unknown template\"}"),
+ ContentType: "application/json",
+ })
+ return resp
+ }
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+func (e *CatalogsVendor) onCatalogs(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Catalogs Vendor", request.Context, &map[string]string{
+ "method": "onCatalogs",
+ })
+ defer span.End()
+
+ lLog.Info("V (Catalogs Vendor): onCatalogs")
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onCatalogs-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ state, err = e.CatalogsManager.ListSpec(ctx)
+ isArray = true
+ } else {
+ state, err = e.CatalogsManager.GetSpec(ctx, id)
+ }
+ if err != nil {
+ if !v1alpha2.IsNotFound(err) {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ } else {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.NotFound,
+ 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("onCatalogs-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+ if id == "" {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte("missing catalog name"),
+ })
+ }
+ var campaign model.CatalogSpec
+
+ err := json.Unmarshal(request.Body, &campaign)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+
+ err = e.CatalogsManager.UpsertSpec(ctx, id, campaign)
+ if err != nil {
+ 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("onCatalogs-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ err := e.CatalogsManager.DeleteSpec(ctx, id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/devices"
+ "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 dLog = logger.NewLogger("coa.runtime")
+
+type DevicesVendor struct {
+ vendors.Vendor
+ DevicesManager *devices.DevicesManager
+}
+
+func (o *DevicesVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Devices",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *DevicesVendor) 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.(*devices.DevicesManager); ok {
+ e.DevicesManager = c
+ }
+ }
+ if e.DevicesManager == nil {
+ return v1alpha2.NewCOAError(nil, "devices manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *DevicesVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "devices"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onDevices,
+ Parameters: []string{"name?"},
+ },
+ }
+}
+
+func (c *DevicesVendor) onDevices(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Devices Vendor", request.Context, &map[string]string{
+ "method": "onDevices",
+ })
+ defer span.End()
+
+ tLog.Info("~ Devices Manager ~ : onDevices")
+
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onDevices-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ state, err = c.DevicesManager.ListSpec(ctx)
+ isArray = true
+ } else {
+ state, err = c.DevicesManager.GetSpec(ctx, id)
+ }
+ if err != nil {
+ 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("onDevices-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+
+ var device model.DeviceSpec
+
+ err := json.Unmarshal(request.Body, &device)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+
+ err = c.DevicesManager.UpsertSpec(ctx, id, device)
+ if err != nil {
+ 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("onDevices-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ err := c.DevicesManager.DeleteSpec(ctx, id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "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/valyala/fasthttp"
+)
+
+type EchoVendor struct {
+ vendors.Vendor
+ myMessages []string
+}
+
+func (o *EchoVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Echo",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *EchoVendor) 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)
+ e.myMessages = make([]string, 0)
+ e.Vendor.Context.Subscribe("trace", func(topic string, event v1alpha2.Event) error {
+ msg := event.Body.(string)
+ e.myMessages = append(e.myMessages, msg)
+ if len(e.myMessages) > 20 {
+ e.myMessages = e.myMessages[1:]
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (o *EchoVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "greetings"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onHello,
+ },
+ }
+}
+
+func (c *EchoVendor) onHello(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ _, span := observability.StartSpan("Echo Vendor", request.Context, &map[string]string{
+ "method": "onHello",
+ })
+ defer span.End()
+
+ switch request.Method {
+ case fasthttp.MethodGet:
+ message := "Hello from Symphony K8s control plane (S8C)"
+ if len(c.myMessages) > 0 {
+ for _, m := range c.myMessages {
+ message = message + "\r\n" + m
+ }
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: []byte(message),
+ ContentType: "application/text",
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, resp)
+ case fasthttp.MethodPost:
+ c.Vendor.Context.Publish("trace", v1alpha2.Event{
+ Body: string(request.Body),
+ })
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "context"
+ "encoding/json"
+ "strconv"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/catalogs"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/sites"
+ "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/trails"
+ "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 fLog = logger.NewLogger("coa.runtime")
+
+type FederationVendor struct {
+ vendors.Vendor
+ SitesManager *sites.SitesManager
+ CatalogsManager *catalogs.CatalogsManager
+ StagingManager *staging.StagingManager
+ SyncManager *sync.SyncManager
+ TrailsManager *trails.TrailsManager
+}
+
+func (f *FederationVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: f.Vendor.Version,
+ Name: "Federation",
+ Producer: "Microsoft",
+ }
+}
+func (f *FederationVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error {
+ err := f.Vendor.Init(config, factories, providers, pubsubProvider)
+ if err != nil {
+ return err
+ }
+ for _, m := range f.Managers {
+ if c, ok := m.(*sites.SitesManager); ok {
+ f.SitesManager = c
+ }
+ if c, ok := m.(*staging.StagingManager); ok {
+ f.StagingManager = c
+ }
+ if c, ok := m.(*catalogs.CatalogsManager); ok {
+ f.CatalogsManager = c
+ }
+ if c, ok := m.(*sync.SyncManager); ok {
+ f.SyncManager = c
+ }
+ if c, ok := m.(*trails.TrailsManager); ok {
+ f.TrailsManager = c
+ }
+ }
+ if f.StagingManager == nil {
+ return v1alpha2.NewCOAError(nil, "staging manager is not supplied", v1alpha2.MissingConfig)
+ }
+ if f.SitesManager == nil {
+ return v1alpha2.NewCOAError(nil, "sites manager is not supplied", v1alpha2.MissingConfig)
+ }
+ if f.CatalogsManager == nil {
+ return v1alpha2.NewCOAError(nil, "catalogs manager is not supplied", v1alpha2.MissingConfig)
+ }
+ f.Vendor.Context.Subscribe("catalog", func(topic string, event v1alpha2.Event) error {
+ sites, err := f.SitesManager.ListSpec(context.TODO())
+ if err != nil {
+ return err
+ }
+ for _, site := range sites {
+ if site.Spec.Name != f.Vendor.Context.SiteInfo.SiteId {
+ event.Metadata["site"] = site.Spec.Name
+ f.StagingManager.HandleJobEvent(context.TODO(), event) //TODO: how to handle errors in this case?
+ }
+ }
+ return nil
+ })
+ f.Vendor.Context.Subscribe("remote", func(topic string, event v1alpha2.Event) error {
+ _, ok := event.Metadata["site"]
+ if !ok {
+ return v1alpha2.NewCOAError(nil, "site is not supplied", v1alpha2.BadRequest)
+ }
+ f.StagingManager.HandleJobEvent(context.TODO(), event) //TODO: how to handle errors in this case?
+ return nil
+ })
+ f.Vendor.Context.Subscribe("report", func(topic string, event v1alpha2.Event) error {
+ fLog.Debugf("V (Federation): received report event: %v", event)
+ jData, _ := json.Marshal(event.Body)
+ var status model.ActivationStatus
+ err := json.Unmarshal(jData, &status)
+ if err == nil {
+ err := utils.SyncActivationStatus(
+ context.TODO(),
+ f.Vendor.Context.SiteInfo.ParentSite.BaseUrl,
+ f.Vendor.Context.SiteInfo.ParentSite.Username,
+ f.Vendor.Context.SiteInfo.ParentSite.Password, status)
+ if err != nil {
+ fLog.Errorf("V (Federation): error while syncing activation status: %v", err)
+ return err
+ }
+ }
+ return v1alpha2.NewCOAError(nil, "report is not an activation status", v1alpha2.BadRequest)
+ })
+ f.Vendor.Context.Subscribe("trail", func(topic string, event v1alpha2.Event) error {
+ if f.TrailsManager != nil {
+ jData, _ := json.Marshal(event.Body)
+ var trails []v1alpha2.Trail
+ err := json.Unmarshal(jData, &trails)
+ if err == nil {
+ return f.TrailsManager.Append(context.TODO(), trails)
+ }
+ }
+ return nil
+ })
+ //now register the current site
+ return f.SitesManager.UpsertSpec(context.TODO(), f.Context.SiteInfo.SiteId, model.SiteSpec{
+ Name: f.Context.SiteInfo.SiteId,
+ Properties: f.Context.SiteInfo.Properties,
+ IsSelf: true,
+ })
+}
+func (f *FederationVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "federation"
+ if f.Route != "" {
+ route = f.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodPost, fasthttp.MethodGet},
+ Route: route + "/sync",
+ Version: f.Version,
+ Handler: f.onSync,
+ Parameters: []string{"site?"},
+ },
+ {
+ Methods: []string{fasthttp.MethodPost, fasthttp.MethodGet},
+ Route: route + "/registry",
+ Version: f.Version,
+ Handler: f.onRegistry,
+ Parameters: []string{"name?"},
+ },
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/status",
+ Version: f.Version,
+ Handler: f.onStatus,
+ Parameters: []string{"name"},
+ },
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/trail",
+ Version: f.Version,
+ Handler: f.onTrail,
+ },
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/k8shook",
+ Version: f.Version,
+ Handler: f.onK8sHook,
+ },
+ }
+}
+func (c *FederationVendor) onStatus(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Federation Vendor", request.Context, &map[string]string{
+ "method": "onStatus",
+ })
+ defer span.End()
+
+ var state model.SiteState
+ json.Unmarshal(request.Body, &state)
+
+ err := c.SitesManager.ReportState(pCtx, state)
+
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+}
+
+func (f *FederationVendor) onRegistry(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Federation Vendor", request.Context, &map[string]string{
+ "method": "onRegistry",
+ })
+ defer span.End()
+
+ tLog.Info("V (Federation): onRegistry")
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onRegistry-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ state, err = f.SitesManager.ListSpec(ctx)
+ isArray = true
+ } else {
+ state, err = f.SitesManager.GetSpec(ctx, id)
+ }
+ if err != nil {
+ 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("onRegistry-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+
+ var site model.SiteSpec
+ err := json.Unmarshal(request.Body, &site)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ //TODO: generate site key pair as needed
+ err = f.SitesManager.UpsertSpec(ctx, id, site)
+ if err != nil {
+ 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("onRegistry-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ err := f.SitesManager.DeleteSpec(ctx, id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+func (f *FederationVendor) onSync(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Federation Vendor", request.Context, &map[string]string{
+ "method": "onSync",
+ })
+ defer span.End()
+
+ tLog.Info("V (Federation): onSync")
+ switch request.Method {
+ case fasthttp.MethodPost:
+ var status model.ActivationStatus
+ err := json.Unmarshal(request.Body, &status)
+ if err != nil {
+ tLog.Errorf("V (Federation): failed to unmarshal activation status: %v", err)
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte(err.Error()),
+ })
+ }
+ err = f.Vendor.Context.Publish("job-report", v1alpha2.Event{
+ Body: status,
+ })
+ if err != nil {
+ tLog.Errorf("V (Federation): failed to publish job report: %v", err)
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ tLog.Debugf("V (Federation): published job report: %v", status)
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onSync-GET", pCtx, nil)
+ id := request.Parameters["__site"]
+ count := request.Parameters["count"]
+ if count == "" {
+ count = "1"
+ }
+ intCount, err := strconv.Atoi(count)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte(err.Error()),
+ })
+ }
+ batch, err := f.StagingManager.GetABatchForSite(id, intCount)
+
+ pack := model.SyncPackage{
+ Origin: f.Context.SiteInfo.SiteId,
+ }
+
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ catalogs := make([]model.CatalogSpec, 0)
+ jobs := make([]v1alpha2.JobData, 0)
+ for _, c := range batch {
+ if c.Action == "RUN" { //TODO: I don't really like this
+ jobs = append(jobs, c)
+ } else {
+ catalog, err := f.CatalogsManager.GetSpec(ctx, c.Id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ catalogs = append(catalogs, *catalog.Spec)
+ }
+ }
+ pack.Catalogs = catalogs
+ pack.Jobs = jobs
+ jData, _ := utils.FormatObject(pack, true, 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
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+func (f *FederationVendor) onTrail(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ _, span := observability.StartSpan("Federation Vendor", request.Context, &map[string]string{
+ "method": "onTrail",
+ })
+ defer span.End()
+
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ return resp
+}
+func (f *FederationVendor) onK8sHook(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ _, span := observability.StartSpan("Federation Vendor", request.Context, &map[string]string{
+ "method": "onK8sHook",
+ })
+ defer span.End()
+
+ tLog.Info("V (Federation): onK8sHook")
+ switch request.Method {
+ case fasthttp.MethodPost:
+ objectType := request.Parameters["objectType"]
+ if objectType == "catalog" {
+ var catalog model.CatalogSpec
+ err := json.Unmarshal(request.Body, &catalog)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte(err.Error()),
+ })
+ }
+ err = f.Vendor.Context.Publish("catalog", v1alpha2.Event{
+ Metadata: map[string]string{
+ "objectType": catalog.Type,
+ },
+ Body: v1alpha2.JobData{
+ Id: catalog.Name,
+ Action: "UPDATE", //TODO: handle deletion, this probably requires BetBachForSites return flags
+ Body: catalog,
+ },
+ })
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ }
+
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+ "strings"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/instances"
+ "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 iLog = logger.NewLogger("coa.runtime")
+
+type InstancesVendor struct {
+ vendors.Vendor
+ InstancesManager *instances.InstancesManager
+}
+
+func (o *InstancesVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Instances",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *InstancesVendor) 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.(*instances.InstancesManager); ok {
+ e.InstancesManager = c
+ }
+ }
+ if e.InstancesManager == nil {
+ return v1alpha2.NewCOAError(nil, "instances manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *InstancesVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "instances"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onInstances,
+ Parameters: []string{"name?"},
+ },
+ }
+}
+
+func (c *InstancesVendor) onInstances(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Instances Vendor", request.Context, &map[string]string{
+ "method": "onInstances",
+ })
+ defer span.End()
+
+ tLog.Info("~ Instances Manager ~ : onInstances")
+
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onInstances-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ 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 {
+ scope = ""
+ }
+ state, err = c.InstancesManager.ListSpec(ctx, scope)
+ isArray = true
+ } else {
+ state, err = c.InstancesManager.GetSpec(ctx, id, scope)
+ }
+ if err != nil {
+ 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("onInstances-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+
+ solution := request.Parameters["solution"]
+ target := request.Parameters["target"]
+ target_selector := request.Parameters["target-selector"]
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ var instance model.InstanceSpec
+
+ if solution != "" && (target != "" || target_selector != "") {
+ instance = model.InstanceSpec{
+ DisplayName: id,
+ Name: id,
+ Solution: solution,
+ }
+ if target != "" {
+ instance.Target = model.TargetSelector{
+ Name: target,
+ }
+ } else {
+ parts := strings.Split(target_selector, "=")
+ if len(parts) != 2 {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte("invalid target selector format. Expected: <property>=<value>"),
+ })
+ }
+ instance.Target = model.TargetSelector{
+ Selector: map[string]string{
+ parts[0]: parts[1],
+ },
+ }
+ }
+ } else {
+ err := json.Unmarshal(request.Body, &instance)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ }
+ err := c.InstancesManager.UpsertSpec(ctx, id, instance, scope)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ if c.Config.Properties["useJobManager"] == "true" {
+ c.Context.Publish("job", v1alpha2.Event{
+ Metadata: map[string]string{
+ "objectType": "instance",
+ "scope": scope,
+ },
+ Body: v1alpha2.JobData{
+ Id: id,
+ Action: "UPDATE",
+ },
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ case fasthttp.MethodDelete:
+ ctx, span := observability.StartSpan("onInstances-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ direct := request.Parameters["direct"]
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ if c.Config.Properties["useJobManager"] == "true" && direct != "true" {
+ c.Context.Publish("job", v1alpha2.Event{
+ Metadata: map[string]string{
+ "objectType": "instance",
+ "scope": scope,
+ },
+ Body: v1alpha2.JobData{
+ Id: id,
+ Action: "DELETE",
+ },
+ })
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ } else {
+ err := c.InstancesManager.DeleteSpec(ctx, id, scope)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/jobs"
+ "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 jLog = logger.NewLogger("coa.runtime")
+
+type JobVendor struct {
+ vendors.Vendor
+ myMessages []string
+ JobsManager *jobs.JobsManager
+}
+
+func (o *JobVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Job",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *JobVendor) 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.(*jobs.JobsManager); ok {
+ e.JobsManager = c
+ }
+ }
+ if e.JobsManager == nil {
+ return v1alpha2.NewCOAError(nil, "jobs manager is not supplied", v1alpha2.MissingConfig)
+ }
+ e.myMessages = make([]string, 0)
+ e.Vendor.Context.Subscribe("trace", func(topic string, event v1alpha2.Event) error {
+ msg := event.Body.(string)
+ e.myMessages = append(e.myMessages, msg)
+ if len(e.myMessages) > 20 {
+ e.myMessages = e.myMessages[1:]
+ }
+ return nil
+ })
+ e.Vendor.Context.Subscribe("job", func(topic string, event v1alpha2.Event) error {
+ err := e.JobsManager.HandleJobEvent(context.Background(), event)
+ if err != nil && v1alpha2.IsDelayed(err) {
+ go e.Vendor.Context.Publish(topic, event)
+ }
+ return err
+ })
+ e.Vendor.Context.Subscribe("heartbeat", func(topic string, event v1alpha2.Event) error {
+ return e.JobsManager.HandleHeartBeatEvent(context.Background(), event)
+ })
+ e.Vendor.Context.Subscribe("schedule", func(topic string, event v1alpha2.Event) error {
+ return e.JobsManager.HandleScheduleEvent(context.Background(), event)
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (o *JobVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "jobs"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onHello,
+ },
+ }
+}
+
+func (c *JobVendor) onHello(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ _, span := observability.StartSpan("Job Vendor", request.Context, &map[string]string{
+ "method": "onHello",
+ })
+ defer span.End()
+
+ switch request.Method {
+ case fasthttp.MethodPost:
+ var activationData v1alpha2.ActivationData
+ err := json.Unmarshal(request.Body, &activationData)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte("{\"result\":\"400 - bad request\"}"),
+ ContentType: "application/json",
+ })
+ }
+ c.Vendor.Context.Publish("activation", v1alpha2.Event{
+ Body: activationData,
+ })
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/models"
+ "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 mLog = logger.NewLogger("coa.runtime")
+
+type ModelsVendor struct {
+ vendors.Vendor
+ ModelsManager *models.ModelsManager
+}
+
+func (o *ModelsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Models",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *ModelsVendor) 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.(*models.ModelsManager); ok {
+ e.ModelsManager = c
+ }
+ }
+ if e.ModelsManager == nil {
+ return v1alpha2.NewCOAError(nil, "models manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *ModelsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "models"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onModels,
+ Parameters: []string{"name?"},
+ },
+ }
+}
+
+func (c *ModelsVendor) onModels(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Models Vendor", request.Context, &map[string]string{
+ "method": "onModels",
+ })
+ defer span.End()
+ tLog.Info("V (Models): onDevices")
+
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onModels-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ state, err = c.ModelsManager.ListSpec(ctx)
+ isArray = true
+ } else {
+ state, err = c.ModelsManager.GetSpec(ctx, id)
+ }
+ if err != nil {
+ 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("onModels-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+
+ var device model.DeviceSpec
+
+ err := json.Unmarshal(request.Body, &device)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+
+ err = c.ModelsManager.UpsertSpec(ctx, id, device)
+ if err != nil {
+ 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("onModels-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ err := c.ModelsManager.DeleteSpec(ctx, id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+ "strings"
+
+ api_utils "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/config"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/pubsub"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/utils"
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
+ "github.com/valyala/fasthttp"
+)
+
+var csLog = logger.NewLogger("coa.runtime")
+
+type SettingsVendor struct {
+ vendors.Vendor
+ EvaluationContext *utils.EvaluationContext
+}
+
+func (e *SettingsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: e.Vendor.Version,
+ Name: "Settings",
+ Producer: "Microsoft",
+ }
+}
+func (e *SettingsVendor) Init(cfg vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error {
+ err := e.Vendor.Init(cfg, factories, providers, pubsubProvider)
+ if err != nil {
+ return err
+ }
+ var configProvider config.IExtConfigProvider
+ for _, m := range e.Managers {
+ if c, ok := m.(config.IExtConfigProvider); ok {
+ configProvider = c
+ }
+ }
+ e.EvaluationContext = &utils.EvaluationContext{
+ ConfigProvider: configProvider,
+ }
+ return nil
+}
+func (e *SettingsVendor) GetEvaluationContext() *utils.EvaluationContext {
+ return e.EvaluationContext
+}
+func (o *SettingsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "settings"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet},
+ Route: route + "/config",
+ Version: o.Version,
+ Handler: o.onConfig,
+ Parameters: []string{"name?"},
+ },
+ }
+}
+
+func (c *SettingsVendor) onConfig(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ _, span := observability.StartSpan("Settings Vendor", request.Context, &map[string]string{
+ "method": "onConfig",
+ })
+ defer span.End()
+ csLog.Info("V (Settings): onConfig")
+ switch request.Method {
+ case fasthttp.MethodGet:
+ id := request.Parameters["__name"]
+ overrides := request.Parameters["overrides"]
+ field := request.Parameters["field"]
+ var parts []string
+ if overrides != "" {
+ parts = strings.Split(overrides, ",")
+ }
+ if field != "" {
+ val, err := c.EvaluationContext.ConfigProvider.Get(id, field, parts, nil)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ data, _ := json.Marshal(val)
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: data,
+ ContentType: "text/plain",
+ })
+ } else {
+ val, err := c.EvaluationContext.ConfigProvider.GetObject(id, parts, nil)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ jData, _ := api_utils.FormatObject(val, false, "", "")
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: jData,
+ ContentType: "application/json",
+ })
+ }
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/skills"
+ "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 kLog = logger.NewLogger("coa.runtime")
+
+type SkillsVendor struct {
+ vendors.Vendor
+ SkillsManager *skills.SkillsManager
+}
+
+func (o *SkillsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Skills",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *SkillsVendor) 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.(*skills.SkillsManager); ok {
+ e.SkillsManager = c
+ }
+ }
+ if e.SkillsManager == nil {
+ return v1alpha2.NewCOAError(nil, "skills manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *SkillsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "skills"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onSkills,
+ Parameters: []string{"name?"},
+ },
+ }
+}
+
+func (c *SkillsVendor) onSkills(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Skills Vendor", request.Context, &map[string]string{
+ "method": "onSkills",
+ })
+ defer span.End()
+ tLog.Info("V (Skills): onSkills")
+
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onSkills-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ state, err = c.SkillsManager.ListSpec(ctx)
+ isArray = true
+ } else {
+ state, err = c.SkillsManager.GetSpec(ctx, id)
+ }
+ if err != nil {
+ 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("onSkills-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+
+ var device model.SkillSpec
+
+ err := json.Unmarshal(request.Body, &device)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+
+ err = c.SkillsManager.UpsertSpec(ctx, id, device)
+ if err != nil {
+ 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("onSkills-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ err := c.SkillsManager.DeleteSpec(ctx, id)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "context"
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solution"
+ "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/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/valyala/fasthttp"
+)
+
+type SolutionVendor struct {
+ vendors.Vendor
+ SolutionManager *solution.SolutionManager
+}
+
+func (o *SolutionVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Solution",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *SolutionVendor) 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.(*solution.SolutionManager); ok {
+ e.SolutionManager = c
+ }
+ }
+ if e.SolutionManager == nil {
+ return v1alpha2.NewCOAError(nil, "solution manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *SolutionVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "solution"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodPost, fasthttp.MethodGet, fasthttp.MethodDelete},
+ Route: route + "/instances", //this route is to support ITargetProvider interface via a proxy provider
+ Version: o.Version,
+ Handler: o.onApplyDeployment,
+ },
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/reconcile",
+ Version: o.Version,
+ Parameters: []string{"delete?"},
+ Handler: o.onReconcile,
+ },
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost},
+ Route: route + "/queue",
+ Version: o.Version,
+ Handler: o.onQueue,
+ },
+ }
+}
+func (c *SolutionVendor) onQueue(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ rContext, span := observability.StartSpan("Solution Vendor", request.Context, &map[string]string{
+ "method": "onQueue",
+ })
+ defer span.End()
+
+ log.Info("V (Solution): onQueue")
+
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onQueue-GET", rContext, nil)
+ defer span.End()
+ instance := request.Parameters["instance"]
+
+ if instance == "" {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte("{\"result\":\"400 - instance parameter is not found\"}"),
+ ContentType: "application/json",
+ })
+ }
+ summary, err := c.SolutionManager.GetSummary(ctx, instance, scope)
+ data, _ := json.Marshal(summary)
+ if err != nil {
+ if v1alpha2.IsNotFound(err) {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.NotFound,
+ Body: data,
+ })
+ } else {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: data,
+ })
+ }
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: data,
+ ContentType: "application/json",
+ })
+ case fasthttp.MethodPost:
+ _, span := observability.StartSpan("onQueue-POST", rContext, nil)
+ defer span.End()
+ instance := request.Parameters["instance"]
+ delete := request.Parameters["delete"]
+ target := request.Parameters["target"]
+ if instance == "" {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte("{\"result\":\"400 - instance parameter is not found\"}"),
+ ContentType: "application/json",
+ })
+ }
+ action := "UPDATE"
+ if delete == "true" {
+ action = "DELETE"
+ }
+ objType := "instance"
+ if target == "true" {
+ objType = "target"
+ }
+ c.Vendor.Context.Publish("job", v1alpha2.Event{
+ Metadata: map[string]string{
+ "objectType": objType,
+ "scope": scope,
+ },
+ Body: v1alpha2.JobData{
+ Id: instance,
+ Action: action,
+ },
+ })
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: []byte("{\"result\":\"200 - instance reconcilation job accepted\"}"),
+ ContentType: "application/json",
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ })
+}
+func (c *SolutionVendor) onReconcile(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ rContext, span := observability.StartSpan("Solution Vendor", request.Context, &map[string]string{
+ "method": "onReconcile",
+ })
+ defer span.End()
+
+ log.Info("V (Solution): onReconcile")
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ switch request.Method {
+ case fasthttp.MethodPost:
+ ctx, span := observability.StartSpan("onReconcile-POST", rContext, nil)
+ defer span.End()
+ var deployment model.DeploymentSpec
+ err := json.Unmarshal(request.Body, &deployment)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ delete := request.Parameters["delete"]
+ summary, err := c.SolutionManager.Reconcile(ctx, deployment, delete == "true", scope)
+ data, _ := json.Marshal(summary)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: data,
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: data,
+ ContentType: "application/json",
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ })
+}
+
+func (c *SolutionVendor) onApplyDeployment(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ _, span := observability.StartSpan("Solution Vendor", request.Context, &map[string]string{
+ "method": "onApplyDeployment",
+ })
+ defer span.End()
+
+ log.Infof("V (Solution): received request %s", request.Method)
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ switch request.Method {
+ case fasthttp.MethodPost:
+ ctx, span := observability.StartSpan("Apply Deployment", request.Context, nil)
+ defer span.End()
+ deployment := new(model.DeploymentSpec)
+ err := json.Unmarshal(request.Body, &deployment)
+ if err != nil {
+ return v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ }
+ }
+ response := c.doDeploy(ctx, *deployment, scope)
+ return observ_utils.CloseSpanWithCOAResponse(span, response)
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("Get Components", request.Context, nil)
+ defer span.End()
+ deployment := new(model.DeploymentSpec)
+ err := json.Unmarshal(request.Body, &deployment)
+ if err != nil {
+ return v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ }
+ }
+ response := c.doGet(ctx, *deployment)
+ return observ_utils.CloseSpanWithCOAResponse(span, response)
+ case fasthttp.MethodDelete:
+ ctx, span := observability.StartSpan("Delete Components", request.Context, nil)
+ defer span.End()
+ var deployment model.DeploymentSpec
+ err := json.Unmarshal(request.Body, &deployment)
+ if err != nil {
+ return v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ }
+ }
+ response := c.doRemove(ctx, deployment, scope)
+ return observ_utils.CloseSpanWithCOAResponse(span, response)
+ }
+
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+func (c *SolutionVendor) doGet(ctx context.Context, deployment model.DeploymentSpec) v1alpha2.COAResponse {
+ ctx, span := observability.StartSpan("Solution Vendor", ctx, &map[string]string{
+ "method": "doGet",
+ })
+ defer span.End()
+ _, components, err := c.SolutionManager.Get(ctx, deployment)
+ if err != nil {
+ response := v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, response)
+ return response
+ }
+ data, _ := json.Marshal(components)
+ response := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: data,
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, response)
+ return response
+}
+func (c *SolutionVendor) doDeploy(ctx context.Context, deployment model.DeploymentSpec, scope string) v1alpha2.COAResponse {
+ ctx, span := observability.StartSpan("Solution Vendor", ctx, &map[string]string{
+ "method": "doDeploy",
+ })
+ defer span.End()
+ summary, err := c.SolutionManager.Reconcile(ctx, deployment, false, scope)
+ data, _ := json.Marshal(summary)
+ if err != nil {
+ response := v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: data,
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, response)
+ return response
+ }
+ response := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: data,
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, response)
+ return response
+}
+func (c *SolutionVendor) doRemove(ctx context.Context, deployment model.DeploymentSpec, scope string) v1alpha2.COAResponse {
+ ctx, span := observability.StartSpan("Solution Vendor", ctx, &map[string]string{
+ "method": "doRemove",
+ })
+ defer span.End()
+
+ summary, err := c.SolutionManager.Reconcile(ctx, deployment, true, scope)
+ data, _ := json.Marshal(summary)
+ if err != nil {
+ response := v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: data,
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, response)
+ return response
+ }
+ response := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: data,
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, response)
+ return response
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/solutions"
+ "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 uLog = logger.NewLogger("coa.runtime")
+
+type SolutionsVendor struct {
+ vendors.Vendor
+ SolutionsManager *solutions.SolutionsManager
+}
+
+func (o *SolutionsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Solutions",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *SolutionsVendor) 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.(*solutions.SolutionsManager); ok {
+ e.SolutionsManager = c
+ }
+ }
+ if e.SolutionsManager == nil {
+ return v1alpha2.NewCOAError(nil, "solutions manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *SolutionsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "solutions"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onSolutions,
+ Parameters: []string{"name?"},
+ },
+ }
+}
+
+func (c *SolutionsVendor) onSolutions(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Solutions Vendor", request.Context, &map[string]string{
+ "method": "onSolutions",
+ })
+ defer span.End()
+ tLog.Info("V (Solutions): onSolutions")
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onSolutions-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ // Change scope back to empty to indicate ListSpec need to query all namespaces
+ if !exist {
+ scope = ""
+ }
+ state, err = c.SolutionsManager.ListSpec(ctx, scope)
+ isArray = true
+ } else {
+ state, err = c.SolutionsManager.GetSpec(ctx, id, scope)
+ }
+ if err != nil {
+ 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("onSolutions-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+
+ embed_type := request.Parameters["embed-type"]
+ embed_component := request.Parameters["embed-component"]
+ embed_property := request.Parameters["embed-property"]
+
+ var solution model.SolutionSpec
+
+ if embed_type != "" && embed_component != "" && embed_property != "" {
+ solution = model.SolutionSpec{
+ DisplayName: id,
+ Components: []model.ComponentSpec{
+ {
+ Name: embed_component,
+ Type: embed_type,
+ Properties: map[string]interface{}{
+ embed_property: string(request.Body),
+ },
+ },
+ },
+ }
+ } else {
+ err := json.Unmarshal(request.Body, &solution)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ }
+ err := c.SolutionsManager.UpsertSpec(ctx, id, solution, scope)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ // TODO: this is a PoC of publishing trails when an object is updated
+ c.Vendor.Context.Publish("trail", v1alpha2.Event{
+ Body: []v1alpha2.Trail{
+ {
+ Origin: c.Vendor.Context.SiteInfo.SiteId,
+ Catalog: solution.Metadata["catalog"],
+ Type: "solutions.solution.symphony/v1",
+ Properties: map[string]interface{}{
+ "spec": solution,
+ },
+ },
+ },
+ Metadata: map[string]string{
+ "scope": scope,
+ },
+ })
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ case fasthttp.MethodDelete:
+ ctx, span := observability.StartSpan("onSolutions-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ err := c.SolutionsManager.DeleteSpec(ctx, id, scope)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/activations"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/campaigns"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/stage"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/materialize"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/mock"
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/providers/stage/wait"
+ "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/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"
+)
+
+var sLog = logger.NewLogger("coa.runtime")
+
+type StageVendor struct {
+ vendors.Vendor
+ StageManager *stage.StageManager
+ CampaignsManager *campaigns.CampaignsManager
+ ActivationsManager *activations.ActivationsManager
+}
+
+func (s *StageVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: s.Vendor.Version,
+ Name: "Stage",
+ Producer: "Microsoft",
+ }
+}
+
+func (o *StageVendor) GetEndpoints() []v1alpha2.Endpoint {
+ return []v1alpha2.Endpoint{}
+}
+
+func (s *StageVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error {
+ err := s.Vendor.Init(config, factories, providers, pubsubProvider)
+ if err != nil {
+ return err
+ }
+ for _, m := range s.Managers {
+ if c, ok := m.(*stage.StageManager); ok {
+ s.StageManager = c
+ }
+ if c, ok := m.(*campaigns.CampaignsManager); ok {
+ s.CampaignsManager = c
+ }
+ if c, ok := m.(*activations.ActivationsManager); ok {
+ s.ActivationsManager = c
+ }
+ }
+ if s.StageManager == nil {
+ return v1alpha2.NewCOAError(nil, "stage manager is not supplied", v1alpha2.MissingConfig)
+ }
+ if s.CampaignsManager == nil {
+ return v1alpha2.NewCOAError(nil, "campaigns manager is not supplied", v1alpha2.MissingConfig)
+ }
+ if s.ActivationsManager == nil {
+ return v1alpha2.NewCOAError(nil, "activations manager is not supplied", v1alpha2.MissingConfig)
+ }
+ s.Vendor.Context.Subscribe("activation", func(topic string, event v1alpha2.Event) error {
+ log.Info("V (Stage): handling activation event")
+ var actData v1alpha2.ActivationData
+ jData, _ := json.Marshal(event.Body)
+ err := json.Unmarshal(jData, &actData)
+ if err != nil {
+ return v1alpha2.NewCOAError(nil, "event body is not an activation job", v1alpha2.BadRequest)
+ }
+ campaign, err := s.CampaignsManager.GetSpec(context.TODO(), actData.Campaign)
+ if err != nil {
+ log.Error("V (Stage): unable to find campaign: %+v", err)
+ return err
+ }
+ activation, err := s.ActivationsManager.GetSpec(context.TODO(), actData.Activation)
+ if err != nil {
+ log.Error("V (Stage): unable to find activation: %+v", err)
+ return err
+ }
+
+ evt, err := s.StageManager.HandleActivationEvent(context.TODO(), actData, *campaign.Spec, activation)
+ if err != nil {
+ return err
+ }
+
+ if evt != nil {
+ s.Vendor.Context.Publish("trigger", v1alpha2.Event{
+ Body: *evt,
+ })
+ }
+ return nil
+ })
+ s.Vendor.Context.Subscribe("trigger", func(topic string, event v1alpha2.Event) error {
+ log.Info("V (Stage): handling trigger event")
+ status := model.ActivationStatus{
+ Stage: "",
+ NextStage: "",
+ Outputs: nil,
+ Status: v1alpha2.Untouched,
+ ErrorMessage: "",
+ IsActive: true,
+ }
+ triggerData := v1alpha2.ActivationData{}
+ jData, _ := json.Marshal(event.Body)
+ err := json.Unmarshal(jData, &triggerData)
+ if err != nil {
+ err = v1alpha2.NewCOAError(nil, "event body is not an activation job", v1alpha2.BadRequest)
+ status.Status = v1alpha2.BadRequest
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ sLog.Errorf("V (Stage): failed to deserialize activation data: %v", err)
+ err = s.ActivationsManager.ReportStatus(context.TODO(), triggerData.Activation, status)
+ if err != nil {
+ sLog.Errorf("V (Stage): failed to report error status: %v (%v)", status.ErrorMessage, err)
+ }
+ }
+ campaign, err := s.CampaignsManager.GetSpec(context.TODO(), triggerData.Campaign)
+ if err != nil {
+ status.Status = v1alpha2.BadRequest
+ status.ErrorMessage = err.Error()
+ status.IsActive = false
+ sLog.Errorf("V (Stage): failed to get campaign spec: %v", err)
+ err = s.ActivationsManager.ReportStatus(context.TODO(), triggerData.Activation, status)
+ if err != nil {
+ sLog.Errorf("V (Stage): failed to report error status: %v (%v)", status.ErrorMessage, err)
+ }
+ }
+ status.Stage = triggerData.Stage
+ status.ActivationGeneration = triggerData.ActivationGeneration
+ status.ErrorMessage = ""
+ status.Status = v1alpha2.Running
+ if triggerData.NeedsReport {
+ sLog.Debugf("V (Stage): reporting status: %v", status)
+ s.Vendor.Context.Publish("report", v1alpha2.Event{
+ Body: status,
+ })
+ } else {
+ err = s.ActivationsManager.ReportStatus(context.TODO(), triggerData.Activation, status)
+ if err != nil {
+ sLog.Errorf("V (Stage): failed to report accepted status: %v (%v)", status.ErrorMessage, err)
+ return err
+ }
+ }
+
+ status, activation := s.StageManager.HandleTriggerEvent(context.TODO(), *campaign.Spec, triggerData)
+
+ if triggerData.NeedsReport {
+ sLog.Debugf("V (Stage): reporting status: %v", status)
+ s.Vendor.Context.Publish("report", v1alpha2.Event{
+ Body: status,
+ })
+
+ } else {
+ err = s.ActivationsManager.ReportStatus(context.TODO(), triggerData.Activation, status)
+ if err != nil {
+ sLog.Errorf("V (Stage): failed to report status: %v (%v)", status.ErrorMessage, err)
+ return err
+ }
+ if activation != nil && status.Status != v1alpha2.Done && status.Status != v1alpha2.Paused {
+ s.Vendor.Context.Publish("trigger", v1alpha2.Event{
+ Body: *activation,
+ })
+ }
+ }
+ log.Info("V (Stage): Finished handling trigger event")
+ return nil
+ })
+ s.Vendor.Context.Subscribe("job-report", func(topic string, event v1alpha2.Event) error {
+ sLog.Debugf("V (Stage): handling job report event: %v", event)
+ jData, _ := json.Marshal(event.Body)
+ var status model.ActivationStatus
+ json.Unmarshal(jData, &status)
+ if status.Status == v1alpha2.Done || status.Status == v1alpha2.OK {
+ campaign, err := s.CampaignsManager.GetSpec(context.TODO(), status.Outputs["__campaign"].(string))
+ if err != nil {
+ sLog.Errorf("V (Stage): failed to get campaign spec '%s': %v", status.Outputs["__campaign"].(string), err)
+ return err
+ }
+ if campaign.Spec.SelfDriving {
+ activation, err := s.StageManager.ResumeStage(status, *campaign.Spec)
+ if err != nil {
+ status.Status = v1alpha2.InternalError
+ status.IsActive = false
+ status.ErrorMessage = fmt.Sprintf("failed to resume stage: %v", err)
+ sLog.Errorf("V (Stage): failed to resume stage: %v", err)
+ }
+ if activation != nil {
+ s.Vendor.Context.Publish("trigger", v1alpha2.Event{
+ Body: *activation,
+ })
+ }
+ }
+ }
+
+ //TODO: later site overrides reports from earlier sites
+ err = s.ActivationsManager.ReportStatus(context.TODO(), status.Outputs["__activation"].(string), status)
+ if err != nil {
+ sLog.Errorf("V (Stage): failed to report status: %v (%v)", status.ErrorMessage, err)
+ return err
+ }
+ return nil
+ })
+ s.Vendor.Context.Subscribe("remote-job", func(topic string, event v1alpha2.Event) error {
+ // Unwrap data package from event body
+ jData, _ := json.Marshal(event.Body)
+ var job v1alpha2.JobData
+ json.Unmarshal(jData, &job)
+ jData, _ = json.Marshal(job.Body)
+ var dataPackage v1alpha2.InputOutputData
+ err := json.Unmarshal(jData, &dataPackage)
+ if err != nil {
+ return err
+ }
+
+ // restore schedule
+ var schedule *v1alpha2.ScheduleSpec
+ if v, ok := dataPackage.Inputs["__schedule"]; ok {
+ err = json.Unmarshal([]byte(v.(string)), &schedule)
+ if err != nil {
+ return err
+ }
+ }
+
+ triggerData := v1alpha2.ActivationData{
+ Activation: dataPackage.Inputs["__activation"].(string),
+ ActivationGeneration: dataPackage.Inputs["__activationGeneration"].(string),
+ Campaign: dataPackage.Inputs["__campaign"].(string),
+ Stage: dataPackage.Inputs["__stage"].(string),
+ Inputs: dataPackage.Inputs,
+ Outputs: dataPackage.Outputs,
+ Schedule: schedule,
+ NeedsReport: true,
+ }
+
+ triggerData.Inputs["__origin"] = event.Metadata["origin"]
+
+ switch dataPackage.Inputs["operation"] {
+ case "wait":
+ triggerData.Provider = "providers.stage.wait"
+ config, err := wait.WaitStageProviderConfigFromVendorMap(s.Vendor.Config.Properties)
+ if err != nil {
+ return err
+ }
+ triggerData.Config = config
+ case "materialize":
+ triggerData.Provider = "providers.stage.materialize"
+ config, err := materialize.MaterializeStageProviderConfigFromVendorMap(s.Vendor.Config.Properties)
+ if err != nil {
+ return err
+ }
+ triggerData.Config = config
+ case "mock":
+ triggerData.Provider = "providers.stage.mock"
+ config, err := mock.MockStageProviderConfigFromMap(s.Vendor.Config.Properties)
+ if err != nil {
+ return err
+ }
+ triggerData.Config = config
+ default:
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("operation %v is not supported", dataPackage.Inputs["operation"]), v1alpha2.BadRequest)
+ }
+ status := s.StageManager.HandleDirectTriggerEvent(context.TODO(), triggerData)
+ sLog.Debugf("V (Stage): reporting status: %v", status)
+ s.Vendor.Context.Publish("report", v1alpha2.Event{
+ Body: status,
+ })
+ return nil
+ })
+ return nil
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "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"
+ "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 gLog = logger.NewLogger("coa.runtime")
+
+type StagingVendor struct {
+ vendors.Vendor
+}
+
+func (f *StagingVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: f.Vendor.Version,
+ Name: "Staging",
+ Producer: "Microsoft",
+ }
+}
+func (f *StagingVendor) Init(config vendors.VendorConfig, factories []managers.IManagerFactroy, providers map[string]map[string]providers.IProvider, pubsubProvider pubsub.IPubSubProvider) error {
+ err := f.Vendor.Init(config, factories, providers, pubsubProvider)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+func (f *StagingVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "federation"
+ if f.Route != "" {
+ route = f.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route + "/download",
+ Version: f.Version,
+ Handler: f.onDownload,
+ },
+ }
+}
+func (f *StagingVendor) onDownload(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ _, span := observability.StartSpan("Staging Vendor", request.Context, &map[string]string{
+ "method": "onDownload",
+ })
+ defer span.End()
+
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+ "strings"
+ "time"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/targets"
+ "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/golang-jwt/jwt/v4"
+ "github.com/valyala/fasthttp"
+)
+
+var tLog = logger.NewLogger("coa.runtime")
+
+type TargetsVendor struct {
+ vendors.Vendor
+ TargetsManager *targets.TargetsManager
+}
+
+func (o *TargetsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Targets",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *TargetsVendor) 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.(*targets.TargetsManager); ok {
+ e.TargetsManager = c
+ }
+ }
+ if e.TargetsManager == nil {
+ return v1alpha2.NewCOAError(nil, "targets manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *TargetsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "targets"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodGet, fasthttp.MethodPost, fasthttp.MethodDelete},
+ Route: route + "/registry",
+ Version: o.Version,
+ Handler: o.onRegistry,
+ Parameters: []string{"name?"},
+ },
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/bootstrap",
+ Version: o.Version,
+ Handler: o.onBootstrap,
+ },
+ {
+ Methods: []string{fasthttp.MethodGet},
+ Route: route + "/ping",
+ Version: o.Version,
+ Handler: o.onHeartBeat,
+ Parameters: []string{"name"},
+ },
+ {
+ Methods: []string{fasthttp.MethodPut},
+ Route: route + "/status",
+ Version: o.Version,
+ Handler: o.onStatus,
+ Parameters: []string{"name", "component?"},
+ },
+ {
+ Methods: []string{fasthttp.MethodGet},
+ Route: route + "/download",
+ Version: o.Version,
+ Handler: o.onDownload,
+ Parameters: []string{"doc-type", "name"},
+ },
+ }
+}
+
+type MyCustomClaims struct {
+ User string `json:"user"`
+ jwt.RegisteredClaims
+}
+type AuthRequest struct {
+ UserName string `json:"username"`
+ Password string `json:"password"`
+}
+
+func (c *TargetsVendor) onRegistry(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Targets Vendor", request.Context, &map[string]string{
+ "method": "onRegistry",
+ })
+ defer span.End()
+ tLog.Info("V (Targets) : onRegistry")
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ switch request.Method {
+ case fasthttp.MethodGet:
+ ctx, span := observability.StartSpan("onRegistry-GET", pCtx, nil)
+ id := request.Parameters["__name"]
+ var err error
+ var state interface{}
+ isArray := false
+ if id == "" {
+ // Change scope back to empty to indicate ListSpec need to query all namespaces
+ if !exist {
+ scope = ""
+ }
+ state, err = c.TargetsManager.ListSpec(ctx, scope)
+ isArray = true
+ } else {
+ state, err = c.TargetsManager.GetSpec(ctx, id, scope)
+ }
+ if err != nil {
+ 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("onRegistry-POST", pCtx, nil)
+ id := request.Parameters["__name"]
+ binding := request.Parameters["with-binding"]
+ var target model.TargetSpec
+ err := json.Unmarshal(request.Body, &target)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ if binding != "" {
+ if binding == "staging" {
+ target.ForceRedeploy = true
+ if target.Topologies == nil {
+ target.Topologies = make([]model.TopologySpec, 0)
+ }
+ found := false
+ for _, t := range target.Topologies {
+ if t.Bindings != nil {
+ for _, b := range t.Bindings {
+ if b.Role == "instance" && b.Provider == "providers.target.staging" {
+ found = true
+ break
+ }
+ }
+ }
+ }
+ if !found {
+ newb := model.BindingSpec{
+ Role: "instance",
+ Provider: "providers.target.staging",
+ Config: map[string]string{
+ "inCluster": "true",
+ "targetName": id,
+ },
+ }
+ if len(target.Topologies) == 0 {
+ target.Topologies = append(target.Topologies, model.TopologySpec{})
+ }
+ if target.Topologies[len(target.Topologies)-1].Bindings == nil {
+ target.Topologies[len(target.Topologies)-1].Bindings = make([]model.BindingSpec, 0)
+ }
+ target.Topologies[len(target.Topologies)-1].Bindings = append(target.Topologies[len(target.Topologies)-1].Bindings, newb)
+ }
+ } else {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.BadRequest,
+ Body: []byte("invalid binding, supported is: 'staging'"),
+ })
+ }
+ }
+ err = c.TargetsManager.UpsertSpec(ctx, id, scope, target)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ if c.Config.Properties["useJobManager"] == "true" {
+ c.Context.Publish("job", v1alpha2.Event{
+ Metadata: map[string]string{
+ "objectType": "target",
+ "scope": scope,
+ },
+ Body: v1alpha2.JobData{
+ Id: id,
+ Action: "UPDATE",
+ },
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ case fasthttp.MethodDelete:
+ ctx, span := observability.StartSpan("onRegistry-DELETE", pCtx, nil)
+ id := request.Parameters["__name"]
+ direct := request.Parameters["direct"]
+
+ if c.Config.Properties["useJobManager"] == "true" && direct != "true" {
+ c.Context.Publish("job", v1alpha2.Event{
+ Metadata: map[string]string{
+ "objectType": "target",
+ "scope": scope,
+ },
+ Body: v1alpha2.JobData{
+ Id: id,
+ Action: "DELETE",
+ },
+ })
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ } else {
+ err := c.TargetsManager.DeleteSpec(ctx, id, scope)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+func (c *TargetsVendor) onBootstrap(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ _, span := observability.StartSpan("Targets Vendor", request.Context, &map[string]string{
+ "method": "onBootstrap",
+ })
+ defer span.End()
+
+ var authRequest AuthRequest
+ err := json.Unmarshal(request.Body, &authRequest)
+ if err != nil || authRequest.UserName != "symphony-test" {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.Unauthorized,
+ Body: []byte(err.Error()),
+ })
+ }
+ mySigningKey := []byte("SymphonyKey")
+ claims := MyCustomClaims{
+ authRequest.UserName,
+ jwt.RegisteredClaims{
+ // A usual scenario is to set the expiration time relative to the current time
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
+ IssuedAt: jwt.NewNumericDate(time.Now()),
+ NotBefore: jwt.NewNumericDate(time.Now()),
+ Issuer: "symphony",
+ Subject: "symphony",
+ ID: "1",
+ Audience: []string{"*"},
+ },
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ ss, _ := token.SignedString(mySigningKey)
+
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: []byte(`{"accessToken":"` + ss + `", "tokenType": "Bearer"}`),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+func (c *TargetsVendor) onStatus(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Targets Vendor", request.Context, &map[string]string{
+ "method": "onStatus",
+ })
+ defer span.End()
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ var dict map[string]interface{}
+ json.Unmarshal(request.Body, &dict)
+
+ properties := make(map[string]string)
+ if k, ok := dict["status"]; ok {
+ var insideKey map[string]interface{}
+ j, _ := json.Marshal(k)
+ json.Unmarshal(j, &insideKey)
+ if p, ok := insideKey["properties"]; ok {
+ jk, _ := json.Marshal(p)
+ json.Unmarshal(jk, &properties)
+ }
+ }
+
+ for k, v := range request.Parameters {
+ if !strings.HasPrefix(k, "__") {
+ properties[k] = v
+ }
+ }
+
+ state, err := c.TargetsManager.ReportState(pCtx, model.TargetState{
+ Id: request.Parameters["__name"],
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FabricGroup,
+ "resource": "targets",
+ "scope": scope,
+ },
+ Status: properties,
+ })
+
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ jData, _ := json.Marshal(state)
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: jData,
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+func (c *TargetsVendor) onDownload(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Targets Vendor", request.Context, &map[string]string{
+ "method": "onDownload",
+ })
+ defer span.End()
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ state, err := c.TargetsManager.GetSpec(pCtx, request.Parameters["__name"], scope)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ jData, err := utils.FormatObject(state, false, request.Parameters["path"], request.Parameters["__doc-type"])
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: jData,
+ ContentType: "application/json",
+ }
+
+ if request.Parameters["__doc-type"] == "yaml" {
+ resp.ContentType = "application/text"
+ }
+
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+func (c *TargetsVendor) onHeartBeat(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Targets Vendor", request.Context, &map[string]string{
+ "method": "onHeartBeat",
+ })
+ defer span.End()
+ scope, exist := request.Parameters["scope"]
+ if !exist {
+ scope = "default"
+ }
+ _, err := c.TargetsManager.ReportState(pCtx, model.TargetState{
+ Id: request.Parameters["__name"],
+ Metadata: map[string]string{
+ "version": "v1",
+ "group": model.FabricGroup,
+ "resource": "targets",
+ "scope": scope,
+ },
+ Status: map[string]string{
+ "ping": time.Now().UTC().String(),
+ },
+ })
+
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: []byte(`{}`),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "encoding/json"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/trails"
+ "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 trLog = logger.NewLogger("coa.runtime")
+
+type TrailsVendor struct {
+ vendors.Vendor
+ TrailsManager *trails.TrailsManager
+}
+
+func (o *TrailsVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Trails",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *TrailsVendor) 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.(*trails.TrailsManager); ok {
+ e.TrailsManager = c
+ }
+ }
+ if e.TrailsManager == nil {
+ return v1alpha2.NewCOAError(nil, "trails manager is not supplied", v1alpha2.MissingConfig)
+ }
+ return nil
+}
+
+func (o *TrailsVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "trails"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route,
+ Version: o.Version,
+ Handler: o.onTrails,
+ },
+ }
+}
+
+func (c *TrailsVendor) onTrails(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ pCtx, span := observability.StartSpan("Trails Vendor", request.Context, &map[string]string{
+ "method": "onTrails",
+ })
+ defer span.End()
+ tLog.Info("V (Trails) : onTrails")
+
+ switch request.Method {
+ case fasthttp.MethodPost:
+ var trails []v1alpha2.Trail
+ err := json.Unmarshal(request.Body, &trails)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ err = c.TrailsManager.Append(pCtx, trails)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.InternalError,
+ Body: []byte(err.Error()),
+ })
+ }
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: []byte("{\"result\":\"ok\"}"),
+ })
+ }
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.MethodNotAllowed,
+ Body: []byte("{\"result\":\"405 - method not allowed\"}"),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/managers/users"
+ "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/golang-jwt/jwt/v4"
+ "github.com/valyala/fasthttp"
+)
+
+var rLog = logger.NewLogger("coa.runtime")
+
+type UsersVendor struct {
+ vendors.Vendor
+ UsersManager *users.UsersManager
+}
+
+func (o *UsersVendor) GetInfo() vendors.VendorInfo {
+ return vendors.VendorInfo{
+ Version: o.Vendor.Version,
+ Name: "Users",
+ Producer: "Microsoft",
+ }
+}
+
+func (e *UsersVendor) 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.(*users.UsersManager); ok {
+ e.UsersManager = c
+ }
+ }
+ if e.UsersManager == nil {
+ return v1alpha2.NewCOAError(nil, "users manager is not supplied", v1alpha2.MissingConfig)
+ }
+ if config.Properties != nil && config.Properties["test-users"] == "true" {
+ e.UsersManager.UpsertUser(context.Background(), "admin", "", nil)
+ e.UsersManager.UpsertUser(context.Background(), "reader", "", nil)
+ e.UsersManager.UpsertUser(context.Background(), "developer", "", nil)
+ e.UsersManager.UpsertUser(context.Background(), "device-manager", "", nil)
+ e.UsersManager.UpsertUser(context.Background(), "operator", "", nil)
+ }
+
+ return nil
+}
+
+func (o *UsersVendor) GetEndpoints() []v1alpha2.Endpoint {
+ route := "users"
+ if o.Route != "" {
+ route = o.Route
+ }
+ return []v1alpha2.Endpoint{
+ {
+ Methods: []string{fasthttp.MethodPost},
+ Route: route + "/auth",
+ Version: o.Version,
+ Handler: o.onAuth,
+ },
+ }
+}
+
+func (c *UsersVendor) onAuth(request v1alpha2.COARequest) v1alpha2.COAResponse {
+ ctx, span := observability.StartSpan("Users Vendor", request.Context, &map[string]string{
+ "method": "onAuth",
+ })
+ defer span.End()
+ log.Debug("V (Users): authenticate user")
+
+ var authRequest AuthRequest
+ err := json.Unmarshal(request.Body, &authRequest)
+ if err != nil {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.Unauthorized,
+ Body: []byte(err.Error()),
+ })
+ }
+ roles, b := c.UsersManager.CheckUser(ctx, authRequest.UserName, authRequest.Password)
+ if !b {
+ return observ_utils.CloseSpanWithCOAResponse(span, v1alpha2.COAResponse{
+ State: v1alpha2.Unauthorized,
+ Body: []byte("login failed"),
+ })
+ }
+
+ mySigningKey := []byte("SymphonyKey")
+ claims := MyCustomClaims{
+ authRequest.UserName,
+ jwt.RegisteredClaims{
+ // A usual scenario is to set the expiration time relative to the current time
+ ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
+ IssuedAt: jwt.NewNumericDate(time.Now()),
+ NotBefore: jwt.NewNumericDate(time.Now()),
+ Issuer: "symphony",
+ Subject: "symphony",
+ ID: "1",
+ Audience: []string{"*"},
+ },
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+ ss, _ := token.SignedString(mySigningKey)
+
+ rolesJSON, _ := json.Marshal(roles)
+ resp := v1alpha2.COAResponse{
+ State: v1alpha2.OK,
+ Body: []byte(fmt.Sprintf(`{"accessToken":"%s", "tokenType": "Bearer", "username": "%s", "roles": %s}`, ss, authRequest.UserName, rolesJSON)),
+ ContentType: "application/json",
+ }
+ observ_utils.UpdateSpanStatusFromCOAResponse(span, resp)
+ return resp
+}
+
+
+
/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package vendors
+
+import (
+ "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/vendors"
+)
+
+type SymphonyVendorFactory struct {
+}
+
+func (c SymphonyVendorFactory) CreateVendor(config vendors.VendorConfig) (vendors.IVendor, error) {
+ switch config.Type {
+ case "vendors.echo":
+ return &EchoVendor{}, nil
+ case "vendors.solution":
+ return &SolutionVendor{}, nil
+ case "vendors.agent":
+ return &AgentVendor{}, nil
+ case "vendors.targets":
+ return &TargetsVendor{}, nil
+ case "vendors.instances":
+ return &InstancesVendor{}, nil
+ case "vendors.devices":
+ return &DevicesVendor{}, nil
+ case "vendors.solutions":
+ return &SolutionsVendor{}, nil
+ case "vendors.campaigns":
+ return &CampaignsVendor{}, nil
+ case "vendors.catalogs":
+ return &CatalogsVendor{}, nil
+ case "vendors.activations":
+ return &ActivationsVendor{}, nil
+ case "vendors.users":
+ return &UsersVendor{}, nil
+ case "vendors.jobs":
+ return &JobVendor{}, nil
+ case "vendors.stage":
+ return &StageVendor{}, nil
+ case "vendors.federation":
+ return &FederationVendor{}, nil
+ case "vendors.staging":
+ return &StagingVendor{}, nil
+ case "vendors.models":
+ return &ModelsVendor{}, nil
+ case "vendors.skills":
+ return &SkillsVendor{}, nil
+ case "vendors.settings":
+ return &SettingsVendor{}, nil
+ case "vendors.trails":
+ return &TrailsVendor{}, nil
+ case "vendors.backgroundjob":
+ return &BackgroundJobVendor{}, nil
+ default:
+ return nil, nil //Can't throw errors as other factories may create it...
+ }
+}
+
+
+
+
+
+
diff --git a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go
index 1b5cecbc0..e8e4aeb9c 100644
--- a/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go
+++ b/api/pkg/apis/v1alpha1/managers/jobs/jobs-manager.go
@@ -356,6 +356,7 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event)
})
var err error = nil
defer observ_utils.CloseSpanWithError(span, &err)
+ log.Info(" M (Job): handling %v event, event body %v, %v", event.Metadata["objectType"], "job", event.Body)
namespace := model.ReadProperty(event.Metadata, "namespace", nil)
if namespace == "" {
@@ -377,7 +378,7 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event)
switch objectType {
case "instance":
- log.Debugf(" M (Job): handling instance job %s", job.Id)
+ log.Debugf(" M (Job): handling instance job >>>>>>>>>>>>>>>>>>>>>>>>>>>> %s, %s", job.Id, namespace)
instanceName := job.Id
var instance model.InstanceState
//get intance
@@ -387,9 +388,13 @@ func (s *JobsManager) HandleJobEvent(ctx context.Context, event v1alpha2.Event)
return err //TODO: instance is gone
}
+ log.Debugf(" M (Job): handling instance job solution name >>>>>>>>>>>>>>>>>>>>>>>>>>>> %s", instance.Spec.Solution)
+
//get solution
var solution model.SolutionState
solution, err = s.apiClient.GetSolution(ctx, instance.Spec.Solution, namespace)
+ log.Debugf(" M (Job): handling instance job solution after GetSolution >>>>>>>>>>>>>>>>>>>>>>>>>>>> %s", solution.ObjectMeta.Name)
+
if err != nil {
solution = model.SolutionState{
ObjectMeta: model.ObjectMeta{
diff --git a/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go b/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go
index 11b484e75..4d86a3173 100644
--- a/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go
+++ b/api/pkg/apis/v1alpha1/managers/solutions/solutions-manager.go
@@ -10,6 +10,8 @@ import (
"context"
"encoding/json"
"fmt"
+ "strconv"
+ "strings"
"github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
"github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2"
@@ -17,11 +19,14 @@ import (
"github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/managers"
"github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers"
"github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/providers/states"
+ "github.com/eclipse-symphony/symphony/coa/pkg/logger"
observability "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability"
observ_utils "github.com/eclipse-symphony/symphony/coa/pkg/apis/v1alpha2/observability/utils"
)
+var sLog = logger.NewLogger("coa.runtime")
+
type SolutionsManager struct {
managers.Manager
StateProvider states.IStateProvider
@@ -48,14 +53,28 @@ func (t *SolutionsManager) DeleteState(ctx context.Context, name string, namespa
var err error = nil
defer observ_utils.CloseSpanWithError(span, &err)
+ var rootResource string
+ var version string
+ parts := strings.Split(name, ":")
+ if len(parts) == 2 {
+ rootResource = parts[0]
+ version = parts[1]
+ } else {
+ return v1alpha2.NewCOAError(nil, fmt.Sprintf("Solution name is invalid in the request (%s)", name), v1alpha2.BadRequest)
+ }
+
+ sLog.Info(" M (Solution manager): delete state >>>>>>>>>>>>>>>>>>>>parts %v, %v", rootResource, version)
+
+ id := rootResource + "-" + version
err = t.StateProvider.Delete(ctx, states.DeleteRequest{
- ID: name,
+ ID: id,
Metadata: map[string]interface{}{
- "namespace": namespace,
- "group": model.SolutionGroup,
- "version": "v1",
- "resource": "solutions",
- "kind": "Solution",
+ "namespace": namespace,
+ "group": model.SolutionGroup,
+ "version": "v1",
+ "resource": "solutions",
+ "kind": "Solution",
+ "rootResource": rootResource,
},
})
return err
@@ -68,28 +87,58 @@ func (t *SolutionsManager) UpsertState(ctx context.Context, name string, state m
var err error = nil
defer observ_utils.CloseSpanWithError(span, &err)
+ sLog.Info(" M (Solution manager): debug upsert state >>>>>>>>>>>>>>>>>>>> %v, %v, %v", state.Spec.Version, state.Spec.RootResource, name)
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)
+ var rootResource string
+ version := state.Spec.Version
+ if state.Spec.RootResource == "" && version != "" {
+ suffix := "-" + version
+ rootResource = strings.TrimSuffix(name, suffix)
+ } else {
+ rootResource = state.Spec.RootResource
+ }
+
+ if state.ObjectMeta.Labels == nil {
+ state.ObjectMeta.Labels = make(map[string]string)
+ }
+
+ _, versionLabelExists := state.ObjectMeta.Labels["version"]
+ _, rootLabelExists := state.ObjectMeta.Labels["rootResource"]
+ refreshLabels := false
+ if !versionLabelExists || !rootLabelExists {
+ sLog.Info(" M (Solution manager): update labels to true >>>>>>>>>>>>>>>>>>>> %v, %v", rootResource, version)
+
+ state.ObjectMeta.Labels["rootResource"] = rootResource
+ state.ObjectMeta.Labels["version"] = version
+ refreshLabels = true
+ }
+
+ sLog.Info(" M (Solution manager): debug refresh >>>>>>>>>>>>>>>>>>>> %v, %v, %v", refreshLabels, versionLabelExists, rootLabelExists)
+
body := map[string]interface{}{
"apiVersion": model.SolutionGroup + "/v1",
"kind": "Solution",
"metadata": state.ObjectMeta,
"spec": state.Spec,
}
+
upsertRequest := states.UpsertRequest{
Value: states.StateEntry{
ID: name,
Body: body,
},
Metadata: map[string]interface{}{
- "namespace": state.ObjectMeta.Namespace,
- "group": model.SolutionGroup,
- "version": "v1",
- "resource": "solutions",
- "kind": "Solution",
+ "namespace": state.ObjectMeta.Namespace,
+ "group": model.SolutionGroup,
+ "version": "v1",
+ "resource": "solutions",
+ "kind": "Solution",
+ "rootResource": rootResource,
+ "refreshLabels": strconv.FormatBool(refreshLabels),
},
}
@@ -150,6 +199,8 @@ func (t *SolutionsManager) GetState(ctx context.Context, id string, namespace st
var err error = nil
defer observ_utils.CloseSpanWithError(span, &err)
+ sLog.Info(" M (Solution manager): debug get state >>>>>>>>>>>>>>>>>>>> %v, %v", id, namespace)
+
getRequest := states.GetRequest{
ID: id,
Metadata: map[string]interface{}{
@@ -172,3 +223,34 @@ func (t *SolutionsManager) GetState(ctx context.Context, id string, namespace st
}
return ret, nil
}
+
+func (t *SolutionsManager) GetLatestState(ctx context.Context, id string, namespace string) (model.SolutionState, error) {
+ ctx, span := observability.StartSpan("Solutions Manager", ctx, &map[string]string{
+ "method": "GetLatest",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" M (Solution manager): debug get latest state >>>>>>>>>>>>>>>>>>>> %v, %v", id, namespace)
+
+ getRequest := states.GetRequest{
+ ID: id,
+ Metadata: map[string]interface{}{
+ "version": "v1",
+ "group": model.SolutionGroup,
+ "resource": "solutions",
+ "namespace": namespace,
+ "kind": "Solution",
+ },
+ }
+ target, err := t.StateProvider.GetLatest(ctx, getRequest)
+ if err != nil {
+ return model.SolutionState{}, err
+ }
+
+ ret, err := getSolutionState(target.Body)
+ if err != nil {
+ return model.SolutionState{}, err
+ }
+ return ret, nil
+}
diff --git a/api/pkg/apis/v1alpha1/model/solution.go b/api/pkg/apis/v1alpha1/model/solution.go
index a266d4e51..0826b0f8d 100644
--- a/api/pkg/apis/v1alpha1/model/solution.go
+++ b/api/pkg/apis/v1alpha1/model/solution.go
@@ -17,9 +17,11 @@ type (
}
SolutionSpec struct {
- DisplayName string `json:"displayName,omitempty"`
- Metadata map[string]string `json:"metadata,omitempty"`
- Components []ComponentSpec `json:"components,omitempty"`
+ DisplayName string `json:"displayName,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Components []ComponentSpec `json:"components,omitempty"`
+ Version string `json:"version,omitempty"`
+ RootResource string `json:"rootResource,omitempty"`
}
)
diff --git a/api/pkg/apis/v1alpha1/model/versionedcampaign.go b/api/pkg/apis/v1alpha1/model/versionedcampaign.go
new file mode 100644
index 000000000..45a467e42
--- /dev/null
+++ b/api/pkg/apis/v1alpha1/model/versionedcampaign.go
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+)
+
+type VersionedCampaignState struct {
+ ObjectMeta ObjectMeta `json:"metadata,omitempty"`
+ Spec *VersionedCampaignSpec `json:"spec,omitempty"`
+}
+
+type VersionedCampaignSpec struct {
+ Name string `json:"name,omitempty"`
+}
+
+func (c VersionedCampaignSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(VersionedCampaignSpec)
+ if !ok {
+ return false, errors.New("parameter is not a VersionedCampaignSpec type")
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+func (c VersionedCampaignState) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(VersionedCampaignState)
+ if !ok {
+ return false, errors.New("parameter is not a VersionedCampaignState type")
+ }
+
+ equal, err := c.ObjectMeta.DeepEquals(otherC.ObjectMeta)
+ if err != nil || !equal {
+ return equal, err
+ }
+
+ equal, err = c.Spec.DeepEquals(*otherC.Spec)
+ if err != nil || !equal {
+ return equal, err
+ }
+
+ return true, nil
+}
diff --git a/api/pkg/apis/v1alpha1/model/versionedcatalog b/api/pkg/apis/v1alpha1/model/versionedcatalog
new file mode 100644
index 000000000..7f0228303
--- /dev/null
+++ b/api/pkg/apis/v1alpha1/model/versionedcatalog
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+ "reflect"
+)
+
+// TODO: all state objects should converge to this paradigm: id, spec and status
+type VersionedCatalogState struct {
+ ObjectMeta ObjectMeta `json:"metadata,omitempty"`
+ Spec *VersionedCatalogSpec `json:"spec,omitempty"`
+}
+
+type VersionedCatalogSpec struct {
+ Name string `json:"name"`
+}
+
+func (c VersionedCatalogSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(VersionedCatalogSpec)
+ if !ok {
+ return false, errors.New("parameter is not a VersionedCatalogSpec type")
+ }
+
+ if c.Name != otherC.Name {
+ return false, nil
+ }
+
+ if c.Generation != otherC.Generation {
+ return false, nil
+ }
+
+ if !reflect.DeepEqual(c.Properties, otherC.Properties) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+func (c VersionedCatalogState) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(VersionedCatalogState)
+ if !ok {
+ return false, errors.New("parameter is not a VersionedCatalogState type")
+ }
+
+ equal, err := c.ObjectMeta.DeepEquals(otherC.ObjectMeta)
+ if err != nil || !equal {
+ return equal, err
+ }
+
+ equal, err = c.Spec.DeepEquals(*otherC.Spec)
+ if err != nil || !equal {
+ return equal, err
+ }
+
+ return true, nil
+}
diff --git a/api/pkg/apis/v1alpha1/model/versionedsolution.go b/api/pkg/apis/v1alpha1/model/versionedsolution.go
new file mode 100644
index 000000000..728a4dbf3
--- /dev/null
+++ b/api/pkg/apis/v1alpha1/model/versionedsolution.go
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+)
+
+type (
+ VersionedSolutionState struct {
+ ObjectMeta ObjectMeta `json:"metadata,omitempty"`
+ Spec *VersionedSolutionSpec `json:"spec,omitempty"`
+ }
+
+ VersionedSolutionSpec struct {
+ DisplayName string `json:"displayName,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ }
+)
+
+func (c VersionedSolutionSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(VersionedSolutionSpec)
+ if !ok {
+ return false, errors.New("parameter is not a VersionedSolutionSpec type")
+ }
+
+ if c.DisplayName != otherC.DisplayName {
+ return false, nil
+ }
+
+ if !StringMapsEqual(c.Metadata, otherC.Metadata, nil) {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+func (c VersionedSolutionState) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(VersionedSolutionState)
+ if !ok {
+ return false, errors.New("parameter is not a VersionedSolutionState type")
+ }
+
+ equal, err := c.ObjectMeta.DeepEquals(otherC.ObjectMeta)
+ if err != nil || !equal {
+ return equal, err
+ }
+
+ equal, err = c.Spec.DeepEquals(*otherC.Spec)
+ if err != nil || !equal {
+ return equal, err
+ }
+
+ return true, nil
+}
diff --git a/api/pkg/apis/v1alpha1/model/versionedtarget.go b/api/pkg/apis/v1alpha1/model/versionedtarget.go
new file mode 100644
index 000000000..99d753b6b
--- /dev/null
+++ b/api/pkg/apis/v1alpha1/model/versionedtarget.go
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ * Licensed under the MIT license.
+ * SPDX-License-Identifier: MIT
+ */
+
+package model
+
+import (
+ "errors"
+ "time"
+)
+
+type (
+ VersionedTargetStatus struct {
+ Properties map[string]string `json:"properties,omitempty"`
+ ProvisioningStatus ProvisioningStatus `json:"provisioningStatus"`
+ LastModified time.Time `json:"lastModified,omitempty"`
+ }
+ // VersionedTargetState defines the current state of the target
+ VersionedTargetState struct {
+ ObjectMeta ObjectMeta `json:"metadata,omitempty"`
+ Status VersionedTargetStatus `json:"status,omitempty"`
+ Spec *VersionedTargetSpec `json:"spec,omitempty"`
+ }
+
+ // VersionedTargetSpec defines the spec property of the VersionedTargetState
+ VersionedTargetSpec struct {
+ DisplayName string `json:"displayName,omitempty"`
+ }
+)
+
+func (c VersionedTargetSpec) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(VersionedTargetSpec)
+ if !ok {
+ return false, errors.New("parameter is not a VersionedTargetSpec type")
+ }
+
+ if c.DisplayName != otherC.DisplayName {
+ return false, nil
+ }
+
+ return true, nil
+}
+
+func (c VersionedTargetState) DeepEquals(other IDeepEquals) (bool, error) {
+ otherC, ok := other.(VersionedTargetState)
+ if !ok {
+ return false, errors.New("parameter is not a VersionedTargetState type")
+ }
+
+ equal, err := c.ObjectMeta.DeepEquals(otherC.ObjectMeta)
+ if err != nil || !equal {
+ return equal, err
+ }
+
+ equal, err = c.Spec.DeepEquals(*otherC.Spec)
+ if err != nil || !equal {
+ return equal, err
+ }
+
+ return true, nil
+}
diff --git a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go
index 47bbf7f6a..6b0e8cf34 100644
--- a/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go
+++ b/api/pkg/apis/v1alpha1/providers/stage/materialize/materialize.go
@@ -179,8 +179,8 @@ func (i *MaterializeStageProvider) Process(ctx context.Context, mgrContext conte
err = utils.UpsertSolution(ctx, i.Config.BaseUrl, solutionState.ObjectMeta.Name, i.Config.User, i.Config.Password, objectData, solutionState.ObjectMeta.Namespace)
if err != nil {
mLog.Errorf("Failed to create solution %s: %s", name, err.Error())
- return outputs, false, err
}
+ return outputs, false, err
creationCount++
case "target":
var targetState model.TargetState
diff --git a/api/pkg/apis/v1alpha1/providers/states/k8s/k8s.go b/api/pkg/apis/v1alpha1/providers/states/k8s/k8s.go
index 172047984..ad0601559 100644
--- a/api/pkg/apis/v1alpha1/providers/states/k8s/k8s.go
+++ b/api/pkg/apis/v1alpha1/providers/states/k8s/k8s.go
@@ -11,7 +11,9 @@ import (
"encoding/json"
"fmt"
"path/filepath"
+ "reflect"
"strconv"
+ "time"
"github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/model"
"github.com/eclipse-symphony/symphony/api/pkg/apis/v1alpha1/utils"
@@ -180,11 +182,23 @@ func (s *K8sStateProvider) Upsert(ctx context.Context, entry states.UpsertReques
version := model.ReadPropertyCompat(entry.Metadata, "version", nil)
resource := model.ReadPropertyCompat(entry.Metadata, "resource", nil)
kind := model.ReadPropertyCompat(entry.Metadata, "kind", nil)
+ rootResource := model.ReadPropertyCompat(entry.Metadata, "rootResource", nil)
+ refreshStr := model.ReadPropertyCompat(entry.Metadata, "refreshLabels", nil)
if namespace == "" {
namespace = "default"
}
+ sLog.Info(" P (K8s State): erefreshStr >>>>>>>>>>>>>>>>>>>> %v ", refreshStr)
+
+ var refreshLabels bool
+ refreshLabels, err = strconv.ParseBool(refreshStr)
+ if err != nil {
+ sLog.Info(" P (K8s State): error parse >>>>>>>>>>>>>>>>>>>> %v", err)
+ refreshLabels = false
+ }
+ sLog.Info(" P (K8s State): upsert state refreshLabels>>>>>>>>>>>>>>>>>>>> %v , %v", refreshLabels, namespace)
+
resourceId := schema.GroupVersionResource{
Group: group,
Version: version,
@@ -196,6 +210,8 @@ func (s *K8sStateProvider) Upsert(ctx context.Context, entry states.UpsertReques
return "", err
}
+ sLog.Info(" P (K8s State): upsert state >>>>>>>>>>>>>>>>>>>> %v , %v", entry.Value.ID, namespace)
+
j, _ := json.Marshal(entry.Value.Body)
item, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).Get(ctx, entry.Value.ID, metav1.GetOptions{})
if err != nil {
@@ -222,9 +238,46 @@ func (s *K8sStateProvider) Upsert(ctx context.Context, entry states.UpsertReques
}
unc.SetName(metadata.Name)
unc.SetNamespace(metadata.Namespace)
- unc.SetLabels(metadata.Labels)
unc.SetAnnotations(metadata.Annotations)
+ if refreshLabels {
+ latestFilterValue := "tag=latest"
+ labelSelector := "rootResource=" + rootResource + "," + latestFilterValue
+ listOptions := metav1.ListOptions{
+ LabelSelector: labelSelector,
+ }
+
+ items, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).List(ctx, listOptions)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to list object with labels %s in namespace %s: %v ", labelSelector, namespace, err)
+ return "", err
+ }
+
+ if len(items.Items) == 0 {
+ sLog.Infof(" P (K8s State): no objects found with labels %s in namespace %s: %v ", labelSelector, namespace, err)
+ }
+
+ for _, v := range items.Items {
+ labels := v.GetLabels()
+ delete(labels, "version")
+ v.SetLabels(labels)
+
+ _, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).Update(ctx, &v, metav1.UpdateOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to remove labels %s from obj %s in namespace %s: %v ", latestFilterValue, v.GetName(), err)
+ return "", err
+ } else {
+ sLog.Infof(" P (K8s State): remove labels %s from object in namespace %s: %v ", labelSelector, v.GetName(), namespace, err)
+ }
+ }
+ if metadata.Labels == nil {
+ metadata.Labels = make(map[string]string)
+ }
+
+ metadata.Labels["tag"] = "latest"
+ unc.SetLabels(metadata.Labels)
+ }
+
_, err = s.DynamicClient.Resource(resourceId).Namespace(namespace).Create(ctx, unc, metav1.CreateOptions{})
if err != nil {
sLog.Errorf(" P (K8s State): failed to create object: %v", err)
@@ -232,6 +285,8 @@ func (s *K8sStateProvider) Upsert(ctx context.Context, entry states.UpsertReques
}
//Note: state is ignored for new object
} else {
+ sLog.Info(" P (K8s State): upsert state exists >>>>>>>>>>>>>>>>>>>> %v , %v", entry.Value.ID, namespace)
+
j, _ := json.Marshal(entry.Value.Body)
var dict map[string]interface{}
err = json.Unmarshal(j, &dict)
@@ -249,15 +304,76 @@ func (s *K8sStateProvider) Upsert(ctx context.Context, entry states.UpsertReques
}
item.SetName(metadata.Name)
item.SetNamespace(metadata.Namespace)
- item.SetLabels(metadata.Labels)
item.SetAnnotations(metadata.Annotations)
+
+ labels := item.GetLabels()
+ if labels == nil {
+ labels = make(map[string]string)
+ }
+
+ for key, value := range metadata.Labels {
+ labels[key] = value
+ }
+ item.SetLabels(labels)
+
+ _, exists := labels["tag"]
+ sLog.Errorf(" P (K8s State): >>>>>>>>> get tag label: efreshLabels, exists, rootResource %v, %v, %v", refreshLabels, exists, rootResource)
+
+ if refreshLabels && !exists {
+ latestFilterValue := "tag=latest"
+ labelSelector := "rootResource=" + rootResource + "," + latestFilterValue
+ sLog.Errorf(" P (K8s State): >>>>>>>>> refresh and not exist: %v", labelSelector)
+
+ listOptions := metav1.ListOptions{
+ LabelSelector: labelSelector,
+ }
+ items, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).List(ctx, listOptions)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to list object with labels %s in namespace %s: %v ", labelSelector, namespace, err)
+ return "", err
+ }
+ if len(items.Items) == 0 {
+ sLog.Infof(" P (K8s State): no objects found with labels %s in namespace %s: %v ", labelSelector, namespace, err)
+ }
+
+ needTag := true
+ currentItemTime := item.GetCreationTimestamp().Time
+ for _, v := range items.Items {
+ sLog.Infof(" P (K8s State): a>>>>>>>>>>>>> v.GetCreationTimestamp() %v ", v.GetCreationTimestamp())
+ if currentItemTime.Before(v.GetCreationTimestamp().Time) {
+ needTag = false
+ } else {
+ labels := v.GetLabels()
+ delete(labels, "tag")
+ v.SetLabels(labels)
+
+ _, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).Update(ctx, &v, metav1.UpdateOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to remove latest label from obj %s in namespace %s: %v ", v.GetName(), err)
+ return "", err
+ } else {
+ sLog.Infof(" P (K8s State): remove latest label from object in namespace %s: %v ", v.GetName(), namespace, err)
+ }
+ }
+ }
+ sLog.Infof(" P (K8s State): a>>>>>>>>>>>>> needtag %s ", needTag)
+
+ if needTag {
+ if metadata.Labels == nil {
+ metadata.Labels = make(map[string]string)
+ }
+ metadata.Labels["tag"] = "latest"
+ item.SetLabels(metadata.Labels)
+ sLog.Infof(" P (K8s State): a>>>>>>>>>>>>> set latest", needTag)
+ }
+ }
}
if v, ok := dict["spec"]; ok {
item.Object["spec"] = v
_, err = s.DynamicClient.Resource(resourceId).Namespace(namespace).Update(ctx, item, metav1.UpdateOptions{})
if err != nil {
- sLog.Errorf(" P (K8s State): failed to update object: %v", err)
+ sLog.Errorf(" P (K8s State): failed to update object for spec: %v", err)
return "", err
}
}
@@ -275,7 +391,7 @@ func (s *K8sStateProvider) Upsert(ctx context.Context, entry states.UpsertReques
status.SetResourceVersion(item.GetResourceVersion())
_, err = s.DynamicClient.Resource(resourceId).Namespace(namespace).UpdateStatus(ctx, status, v1.UpdateOptions{})
if err != nil {
- sLog.Errorf(" P (K8s State): failed to update object status: %v", err)
+ sLog.Errorf(" P (K8s State): failed to update object status for status: %v", err)
return "", err
}
}
@@ -417,12 +533,19 @@ func (s *K8sStateProvider) Delete(ctx context.Context, request states.DeleteRequ
var err error = nil
defer observ_utils.CloseSpanWithError(span, &err)
- sLog.Info(" P (K8s State): delete state")
+ sLog.Info(" P (K8s State): delete state %v", request.ID)
namespace := model.ReadPropertyCompat(request.Metadata, "namespace", nil)
group := model.ReadPropertyCompat(request.Metadata, "group", nil)
version := model.ReadPropertyCompat(request.Metadata, "version", nil)
resource := model.ReadPropertyCompat(request.Metadata, "resource", nil)
+ rootResource := model.ReadPropertyCompat(request.Metadata, "rootResource", nil)
+
+ if namespace == "" {
+ namespace = "default"
+ }
+
+ sLog.Info(" P (K8s State): delete state >>>>>>>>>>>>>>>>>>>> %v", request.ID)
resourceId := schema.GroupVersionResource{
Group: group,
@@ -438,11 +561,73 @@ func (s *K8sStateProvider) Delete(ctx context.Context, request states.DeleteRequ
return err
}
- err = s.DynamicClient.Resource(resourceId).Namespace(namespace).Delete(ctx, request.ID, metav1.DeleteOptions{})
- if err != nil {
- sLog.Errorf(" P (K8s State): failed to delete objects: %v", err)
- return err
+ item, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).Get(ctx, request.ID, metav1.GetOptions{})
+ if err == nil {
+ sLog.Info(" P (K8s State): delete state , get object>>>>>>>>>>>>>>>>>>>> %v", request.ID)
+
+ labels := item.GetLabels()
+ _, exists := labels["tag"]
+
+ if exists && labels["tag"] == "latest" {
+ labelSelector := "rootResource=" + rootResource
+ listOptions := metav1.ListOptions{
+ LabelSelector: labelSelector,
+ }
+ items, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).List(ctx, listOptions)
+ sLog.Info(" P (K8s State): delete state , list items acount >>>>>>>>>>>>>>>>>>>> %d", len(items.Items))
+
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to list object with labels %s in namespace %s: %v ", labelSelector, namespace, err)
+ return err
+ }
+
+ var latestItem unstructured.Unstructured
+ var latestTime time.Time
+ for _, v := range items.Items {
+ if reflect.DeepEqual(item, &v) {
+ sLog.Info(" P (K8s State): delete state , deep copy equal>>>>>>>>>>>>>>>>>>>> %v", item.GetName())
+ continue
+ }
+ if latestTime.Before(v.GetCreationTimestamp().Time) {
+ latestTime = v.GetCreationTimestamp().Time
+ sLog.Info(" P (K8s State): delete state , latest item refreshed1 >>>>>>>>>>>>>>>>>>>> %v", v.GetName())
+ latestItem = v
+ }
+ }
+
+ if !reflect.DeepEqual(latestItem, unstructured.Unstructured{}) {
+ labels := latestItem.GetLabels()
+ if labels == nil {
+ labels = make(map[string]string)
+ }
+ _, existTag := labels["tag"]
+ sLog.Info(" P (K8s State): delete state , latest exist tag >>>>>>>>>>>>>>>>>>>> %v", existTag)
+
+ if !existTag {
+ labels["tag"] = "latest"
+ latestItem.SetLabels(labels)
+
+ sLog.Info(" P (K8s State): delete state , update latest item>>>>>>>>>>>>>>>>>>>> %v", labels["tag"])
+ _, err = s.DynamicClient.Resource(resourceId).Namespace(namespace).Update(ctx, &latestItem, metav1.UpdateOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to add labels for obj %s in namespace %s: %v ", latestItem.GetName(), err)
+ return err
+ } else {
+ sLog.Infof(" P (K8s State): add labels %s from object %s in namespace %s: %v ", labelSelector, latestItem.GetName(), namespace, err)
+ }
+ }
+ }
+
+ }
+
+ sLog.Info(" P (K8s State): delete state , delete the current %v", request.ID)
+ err = s.DynamicClient.Resource(resourceId).Namespace(namespace).Delete(ctx, request.ID, metav1.DeleteOptions{})
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to delete objects: %v", err)
+ return err
+ }
}
+
return nil
}
@@ -453,13 +638,15 @@ func (s *K8sStateProvider) Get(ctx context.Context, request states.GetRequest) (
var err error = nil
defer observ_utils.CloseSpanWithError(span, &err)
- sLog.Info(" P (K8s State): get state")
+ sLog.Info(" P (K8s State): get state ")
namespace := model.ReadPropertyCompat(request.Metadata, "namespace", nil)
group := model.ReadPropertyCompat(request.Metadata, "group", nil)
version := model.ReadPropertyCompat(request.Metadata, "version", nil)
resource := model.ReadPropertyCompat(request.Metadata, "resource", nil)
+ sLog.Info(" P (K8s State): get state >>>>>>>>>>>>>>>>>>>> %v", request.ID)
+
if namespace == "" {
namespace = "default"
}
@@ -506,6 +693,90 @@ func (s *K8sStateProvider) Get(ctx context.Context, request states.GetRequest) (
return ret, nil
}
+func (s *K8sStateProvider) GetLatest(ctx context.Context, request states.GetRequest) (states.StateEntry, error) {
+ ctx, span := observability.StartSpan("K8s State Provider", ctx, &map[string]string{
+ "method": "GetLatest",
+ })
+ var err error = nil
+ defer observ_utils.CloseSpanWithError(span, &err)
+
+ sLog.Info(" P (K8s State): get state with latest label")
+
+ namespace := model.ReadPropertyCompat(request.Metadata, "namespace", nil)
+ group := model.ReadPropertyCompat(request.Metadata, "group", nil)
+ version := model.ReadPropertyCompat(request.Metadata, "version", nil)
+ resource := model.ReadPropertyCompat(request.Metadata, "resource", nil)
+
+ sLog.Info(" P (K8s State): debug get GetLatest >>>>>>>>>>>>>>>>>>>> %v", request.ID)
+
+ if namespace == "" {
+ namespace = "default"
+ }
+
+ resourceId := schema.GroupVersionResource{
+ Group: group,
+ Version: version,
+ Resource: resource,
+ }
+
+ if request.ID == "" {
+ err := v1alpha2.NewCOAError(nil, "found invalid request ID", v1alpha2.BadRequest)
+ return states.StateEntry{}, err
+ }
+
+ latestFilterValue := "tag=latest"
+ labelSelector := "rootResource=" + request.ID + "," + latestFilterValue
+ options := metav1.ListOptions{
+ LabelSelector: labelSelector,
+ }
+
+ sLog.Info(" P (K8s State): debug get GetLatest label selector >>>>>>>>>>>>>>>>>>>> %v", labelSelector)
+
+ items, err := s.DynamicClient.Resource(resourceId).Namespace(namespace).List(ctx, options)
+ if err != nil {
+ sLog.Errorf(" P (K8s State): failed to get latest object %s in namespace %s: %v ", request.ID, namespace, err)
+ return states.StateEntry{}, err
+ }
+ sLog.Info(" P (K8s State): debug get GetLatest list resource count >>>>>>>>>>>>>>>>>>>> %d", len(items.Items))
+
+ var latestItem unstructured.Unstructured
+ var latestTime time.Time
+
+ if len(items.Items) == 0 {
+ sLog.Info(" P (K8s State): debug get GetLatest get 0 latest object >>>>>>>>>>>>>>>>>>>> %d", len(items.Items))
+ err := v1alpha2.NewCOAError(nil, "failed to find latest object", v1alpha2.NotFound)
+ return states.StateEntry{}, err
+ }
+
+ for _, v := range items.Items {
+ if latestTime.Before(v.GetCreationTimestamp().Time) {
+ sLog.Info(" P (K8s State): debug get GetLatest set latest >>>>>>>>>>>>>>>>>>>> %d", v.GetName())
+ latestTime = v.GetCreationTimestamp().Time
+ latestItem = v
+ }
+ }
+
+ generation := latestItem.GetGeneration()
+
+ metadata := model.ObjectMeta{
+ Name: latestItem.GetName(),
+ Namespace: latestItem.GetNamespace(),
+ Labels: latestItem.GetLabels(),
+ Annotations: latestItem.GetAnnotations(),
+ }
+
+ ret := states.StateEntry{
+ ID: request.ID,
+ ETag: strconv.FormatInt(generation, 10),
+ Body: map[string]interface{}{
+ "spec": latestItem.Object["spec"],
+ "status": latestItem.Object["status"],
+ "metadata": metadata,
+ },
+ }
+ return ret, nil
+}
+
// Implmeement the IConfigProvider interface
func (s *K8sStateProvider) Read(object string, field string) (string, error) {
obj, err := s.Get(context.TODO(), states.GetRequest{
diff --git a/api/pkg/apis/v1alpha1/providers/target/k8s/coverage.html b/api/pkg/apis/v1alpha1/providers/target/k8s/coverage.html
new file mode 100644
index 000000000..e35a06ddc
--- /dev/null
+++ b/api/pkg/apis/v1alpha1/providers/target/k8s/coverage.html
@@ -0,0 +1,1123 @@
+
+
+
+
+