Selaa lähdekoodia

Merge branch 'master' of github.com:porter-dev/porter into nico/expanded-chart-overhaul

jnfrati 4 vuotta sitten
vanhempi
sitoutus
b26778736c

+ 21 - 0
api/client/k8s.go

@@ -91,6 +91,27 @@ func (c *Client) GetEnvGroup(
 	return resp, err
 	return resp, err
 }
 }
 
 
+func (c *Client) CloneEnvGroup(
+	ctx context.Context,
+	projectID, clusterID uint,
+	namespace string,
+	req *types.CloneEnvGroupRequest,
+) (*types.EnvGroup, error) {
+	resp := &types.EnvGroup{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/namespaces/%s/envgroup/clone",
+			projectID, clusterID,
+			namespace,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}
+
 func (c *Client) GetRelease(
 func (c *Client) GetRelease(
 	ctx context.Context,
 	ctx context.Context,
 	projectID, clusterID uint,
 	projectID, clusterID uint,

+ 2 - 2
api/server/handlers/namespace/clone_env_group.go

@@ -46,7 +46,7 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 		return
 	}
 	}
 
 
-	envGroup, err := envgroup.GetEnvGroup(agent, request.Name, request.Namespace, request.Version)
+	envGroup, err := envgroup.GetEnvGroup(agent, request.Name, namespace, request.Version)
 
 
 	if err != nil {
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
@@ -59,7 +59,7 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 
 	configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
 	configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
 		Name:      request.CloneName,
 		Name:      request.CloneName,
-		Namespace: namespace,
+		Namespace: request.Namespace,
 		Variables: envGroup.Variables,
 		Variables: envGroup.Variables,
 	})
 	})
 
 

+ 9 - 9
api/server/handlers/namespace/get_env_group.go

@@ -1,9 +1,9 @@
 package namespace
 package namespace
 
 
 import (
 import (
-	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"strings"
 
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -11,7 +11,6 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 )
 )
@@ -51,13 +50,14 @@ func (c *GetEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 
 	envGroup, err := envgroup.GetEnvGroup(agent, request.Name, namespace, request.Version)
 	envGroup, err := envgroup.GetEnvGroup(agent, request.Name, namespace, request.Version)
 
 
-	if err != nil && errors.Is(err, kubernetes.IsNotFoundError) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("env group not found"),
-			http.StatusNotFound,
-		))
-		return
-	} else if err != nil {
+	if err != nil {
+		if strings.Contains(err.Error(), "not found") {
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+				fmt.Errorf("env group not found"),
+				http.StatusNotFound),
+			)
+			return
+		}
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 		return
 	}
 	}

+ 6 - 6
api/server/router/namespace.go

@@ -87,14 +87,14 @@ func getNamespaceRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/envgroups/list -> namespace.NewListEnvGroupsHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/envgroup/list -> namespace.NewListEnvGroupsHandler
 	listEnvGroupsEndpoint := factory.NewAPIEndpoint(
 	listEnvGroupsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 			Path: &types.Path{
 				Parent:       basePath,
 				Parent:       basePath,
-				RelativePath: relPath + "/envgroups/list",
+				RelativePath: relPath + "/envgroup/list",
 			},
 			},
 			Scopes: []types.PermissionScope{
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.UserScope,
@@ -116,14 +116,14 @@ func getNamespaceRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
-	// GET /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/envgroups/clone -> namespace.NewCloneEnvGroupHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/envgroup/clone -> namespace.NewCloneEnvGroupHandler
 	cloneEnvGroupEndpoint := factory.NewAPIEndpoint(
 	cloneEnvGroupEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbGet,
-			Method: types.HTTPVerbGet,
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
 			Path: &types.Path{
 			Path: &types.Path{
 				Parent:       basePath,
 				Parent:       basePath,
-				RelativePath: relPath + "/envgroups/clone",
+				RelativePath: relPath + "/envgroup/clone",
 			},
 			},
 			Scopes: []types.PermissionScope{
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.UserScope,

+ 150 - 57
cli/cmd/apply.go

@@ -98,13 +98,13 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []str
 	worker.RegisterDriver("porter.deploy", NewPorterDriver)
 	worker.RegisterDriver("porter.deploy", NewPorterDriver)
 	worker.SetDefaultDriver("porter.deploy")
 	worker.SetDefaultDriver("porter.deploy")
 
 
-	deplNamespace := os.Getenv("PORTER_NAMESPACE")
+	if hasDeploymentHookEnvVars() {
+		deplNamespace := os.Getenv("PORTER_NAMESPACE")
 
 
-	if deplNamespace == "" {
-		return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
-	}
+		if deplNamespace == "" {
+			return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
+		}
 
 
-	if hasDeploymentHookEnvVars() {
 		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
 		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
 
 
 		if err != nil {
 		if err != nil {
@@ -114,6 +114,9 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, args []str
 		worker.RegisterHook("deployment", deploymentHook)
 		worker.RegisterHook("deployment", deploymentHook)
 	}
 	}
 
 
+	cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
+	worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
+
 	return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
 	return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
 		BasePath: basePath,
 		BasePath: basePath,
 	})
 	})
@@ -178,7 +181,7 @@ type ApplicationConfig struct {
 		Buildpacks []string
 		Buildpacks []string
 	}
 	}
 
 
-	EnvGroups []string
+	EnvGroups []types.EnvGroupMeta
 
 
 	Values map[string]interface{}
 	Values map[string]interface{}
 }
 }
@@ -198,18 +201,24 @@ func NewPorterDriver(resource *models.Resource, opts *drivers.SharedDriverOpts)
 		output:      make(map[string]interface{}),
 		output:      make(map[string]interface{}),
 	}
 	}
 
 
-	err := driver.getSource(resource.Source)
+	source := &Source{}
 
 
+	err := getSource(resource.Source, source)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	err = driver.getTarget(resource.Target)
+	driver.source = source
 
 
+	target := &Target{}
+
+	err = getTarget(resource.Target, target)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	driver.target = target
+
 	return driver, nil
 	return driver, nil
 }
 }
 
 
@@ -535,94 +544,90 @@ func (d *Driver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 	return d.output, nil
 }
 }
 
 
-func (d *Driver) getSource(genericSource map[string]interface{}) error {
-	d.source = &Source{}
-
+func getSource(input map[string]interface{}, output *Source) error {
 	// first read from env vars
 	// first read from env vars
-	d.source.Name = os.Getenv("PORTER_SOURCE_NAME")
-	d.source.Repo = os.Getenv("PORTER_SOURCE_REPO")
-	d.source.Version = os.Getenv("PORTER_SOURCE_VERSION")
+	output.Name = os.Getenv("PORTER_SOURCE_NAME")
+	output.Repo = os.Getenv("PORTER_SOURCE_REPO")
+	output.Version = os.Getenv("PORTER_SOURCE_VERSION")
 
 
 	// next, check for values in the YAML file
 	// next, check for values in the YAML file
-	if d.source.Name == "" {
-		if name, ok := genericSource["name"]; ok {
+	if output.Name == "" {
+		if name, ok := input["name"]; ok {
 			nameVal, ok := name.(string)
 			nameVal, ok := name.(string)
 			if !ok {
 			if !ok {
 				return fmt.Errorf("invalid name provided")
 				return fmt.Errorf("invalid name provided")
 			}
 			}
-			d.source.Name = nameVal
+			output.Name = nameVal
 		}
 		}
 	}
 	}
 
 
-	if d.source.Name == "" {
+	if output.Name == "" {
 		return fmt.Errorf("source name required")
 		return fmt.Errorf("source name required")
 	}
 	}
 
 
-	if d.source.Repo == "" {
-		if repo, ok := genericSource["repo"]; ok {
+	if output.Repo == "" {
+		if repo, ok := input["repo"]; ok {
 			repoVal, ok := repo.(string)
 			repoVal, ok := repo.(string)
 			if !ok {
 			if !ok {
 				return fmt.Errorf("invalid repo provided")
 				return fmt.Errorf("invalid repo provided")
 			}
 			}
-			d.source.Repo = repoVal
+			output.Repo = repoVal
 		}
 		}
 	}
 	}
 
 
-	if d.source.Version == "" {
-		if version, ok := genericSource["version"]; ok {
+	if output.Version == "" {
+		if version, ok := input["version"]; ok {
 			versionVal, ok := version.(string)
 			versionVal, ok := version.(string)
 			if !ok {
 			if !ok {
 				return fmt.Errorf("invalid version provided")
 				return fmt.Errorf("invalid version provided")
 			}
 			}
-			d.source.Version = versionVal
+			output.Version = versionVal
 		}
 		}
 	}
 	}
 
 
 	// lastly, just put in the defaults
 	// lastly, just put in the defaults
-	if d.source.Version == "" {
-		d.source.Version = "latest"
+	if output.Version == "" {
+		output.Version = "latest"
 	}
 	}
 
 
-	d.source.IsApplication = d.source.Repo == "https://charts.getporter.dev"
+	output.IsApplication = output.Repo == "https://charts.getporter.dev"
 
 
-	if d.source.Repo == "" {
-		d.source.Repo = "https://charts.getporter.dev"
+	if output.Repo == "" {
+		output.Repo = "https://charts.getporter.dev"
 
 
-		values, err := existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
+		values, err := existsInRepo(output.Name, output.Version, output.Repo)
 
 
 		if err == nil {
 		if err == nil {
 			// found in "https://charts.getporter.dev"
 			// found in "https://charts.getporter.dev"
-			d.source.SourceValues = values
-			d.source.IsApplication = true
+			output.SourceValues = values
+			output.IsApplication = true
 			return nil
 			return nil
 		}
 		}
 
 
-		d.source.Repo = "https://chart-addons.getporter.dev"
+		output.Repo = "https://chart-addons.getporter.dev"
 
 
-		values, err = existsInRepo(d.source.Name, d.source.Version, d.source.Repo)
+		values, err = existsInRepo(output.Name, output.Version, output.Repo)
 
 
 		if err == nil {
 		if err == nil {
 			// found in https://chart-addons.getporter.dev
 			// found in https://chart-addons.getporter.dev
-			d.source.SourceValues = values
+			output.SourceValues = values
 			return nil
 			return nil
 		}
 		}
 
 
 		return fmt.Errorf("source does not exist in any repo")
 		return fmt.Errorf("source does not exist in any repo")
 	}
 	}
 
 
-	return fmt.Errorf("source '%s' does not exist in repo '%s'", d.source.Name, d.source.Repo)
+	return fmt.Errorf("source '%s' does not exist in repo '%s'", output.Name, output.Repo)
 }
 }
 
 
-func (d *Driver) getTarget(genericTarget map[string]interface{}) error {
-	d.target = &Target{}
-
+func getTarget(input map[string]interface{}, output *Target) error {
 	// first read from env vars
 	// first read from env vars
 	if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
 	if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {
 		project, err := strconv.Atoi(projectEnv)
 		project, err := strconv.Atoi(projectEnv)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		d.target.Project = uint(project)
+		output.Project = uint(project)
 	}
 	}
 
 
 	if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
 	if clusterEnv := os.Getenv("PORTER_CLUSTER"); clusterEnv != "" {
@@ -630,51 +635,51 @@ func (d *Driver) getTarget(genericTarget map[string]interface{}) error {
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		d.target.Cluster = uint(cluster)
+		output.Cluster = uint(cluster)
 	}
 	}
 
 
-	d.target.Namespace = os.Getenv("PORTER_NAMESPACE")
+	output.Namespace = os.Getenv("PORTER_NAMESPACE")
 
 
 	// next, check for values in the YAML file
 	// next, check for values in the YAML file
-	if d.target.Project == 0 {
-		if project, ok := genericTarget["project"]; ok {
+	if output.Project == 0 {
+		if project, ok := input["project"]; ok {
 			projectVal, ok := project.(uint)
 			projectVal, ok := project.(uint)
 			if !ok {
 			if !ok {
 				return fmt.Errorf("project value must be an integer")
 				return fmt.Errorf("project value must be an integer")
 			}
 			}
-			d.target.Project = projectVal
+			output.Project = projectVal
 		}
 		}
 	}
 	}
 
 
-	if d.target.Cluster == 0 {
-		if cluster, ok := genericTarget["cluster"]; ok {
+	if output.Cluster == 0 {
+		if cluster, ok := input["cluster"]; ok {
 			clusterVal, ok := cluster.(uint)
 			clusterVal, ok := cluster.(uint)
 			if !ok {
 			if !ok {
 				return fmt.Errorf("cluster value must be an integer")
 				return fmt.Errorf("cluster value must be an integer")
 			}
 			}
-			d.target.Cluster = clusterVal
+			output.Cluster = clusterVal
 		}
 		}
 	}
 	}
 
 
-	if d.target.Namespace == "" {
-		if namespace, ok := genericTarget["namespace"]; ok {
+	if output.Namespace == "" {
+		if namespace, ok := input["namespace"]; ok {
 			namespaceVal, ok := namespace.(string)
 			namespaceVal, ok := namespace.(string)
 			if !ok {
 			if !ok {
 				return fmt.Errorf("invalid namespace provided")
 				return fmt.Errorf("invalid namespace provided")
 			}
 			}
-			d.target.Namespace = namespaceVal
+			output.Namespace = namespaceVal
 		}
 		}
 	}
 	}
 
 
 	// lastly, just put in the defaults
 	// lastly, just put in the defaults
-	if d.target.Project == 0 {
-		d.target.Project = config.Project
+	if output.Project == 0 {
+		output.Project = config.Project
 	}
 	}
-	if d.target.Cluster == 0 {
-		d.target.Cluster = config.Cluster
+	if output.Cluster == 0 {
+		output.Cluster = config.Cluster
 	}
 	}
-	if d.target.Namespace == "" {
-		d.target.Namespace = "default"
+	if output.Namespace == "" {
+		output.Namespace = "default"
 	}
 	}
 
 
 	return nil
 	return nil
@@ -931,3 +936,91 @@ func (t *DeploymentHook) OnError(err error) {
 		)
 		)
 	}
 	}
 }
 }
+
+type CloneEnvGroupHook struct {
+	client   *api.Client
+	resGroup *switchboardTypes.ResourceGroup
+}
+
+func NewCloneEnvGroupHook(client *api.Client, resourceGroup *switchboardTypes.ResourceGroup) *CloneEnvGroupHook {
+	return &CloneEnvGroupHook{
+		client:   client,
+		resGroup: resourceGroup,
+	}
+}
+
+func (t *CloneEnvGroupHook) PreApply() error {
+	for _, res := range t.resGroup.Resources {
+		config := &ApplicationConfig{}
+
+		err := mapstructure.Decode(res.Config, &config)
+		if err != nil {
+			continue
+		}
+
+		if config != nil && len(config.EnvGroups) > 0 {
+			target := &Target{}
+
+			err = getTarget(res.Target, target)
+
+			if err != nil {
+				return err
+			}
+
+			for _, group := range config.EnvGroups {
+				if group.Name == "" {
+					return fmt.Errorf("env group name cannot be empty")
+				}
+
+				_, err := t.client.GetEnvGroup(
+					context.Background(),
+					target.Project,
+					target.Cluster,
+					target.Namespace,
+					&types.GetEnvGroupRequest{
+						Name:    group.Name,
+						Version: group.Version,
+					},
+				)
+
+				if err != nil && err.Error() == "env group not found" {
+					if group.Namespace == "" {
+						return fmt.Errorf("env group namespace cannot be empty")
+					}
+
+					color.New(color.FgBlue, color.Bold).
+						Printf("Env group '%s' does not exist in the target namespace '%s'\n", group.Name, target.Namespace)
+					color.New(color.FgBlue, color.Bold).
+						Printf("Cloning env group '%s' from namespace '%s' to target namespace '%s'\n",
+							group.Name, group.Namespace, target.Namespace)
+
+					_, err = t.client.CloneEnvGroup(
+						context.Background(), target.Project, target.Cluster, group.Namespace,
+						&types.CloneEnvGroupRequest{
+							Name:      group.Name,
+							Namespace: target.Namespace,
+						},
+					)
+
+					if err != nil {
+						return err
+					}
+				} else if err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func (t *CloneEnvGroupHook) DataQueries() map[string]interface{} {
+	return nil
+}
+
+func (t *CloneEnvGroupHook) PostApply(populatedData map[string]interface{}) error {
+	return nil
+}
+
+func (t *CloneEnvGroupHook) OnError(err error) {}

+ 7 - 32
cli/cmd/deploy/shared.go

@@ -2,8 +2,7 @@ package deploy
 
 
 import (
 import (
 	"context"
 	"context"
-	"strconv"
-	"strings"
+	"fmt"
 
 
 	api "github.com/porter-dev/porter/api/client"
 	api "github.com/porter-dev/porter/api/client"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
@@ -19,43 +18,19 @@ type SharedOpts struct {
 	OverrideTag     string
 	OverrideTag     string
 	Method          DeployBuildType
 	Method          DeployBuildType
 	AdditionalEnv   map[string]string
 	AdditionalEnv   map[string]string
-	EnvGroups       []string
-}
-
-func getEnvGroupNameVersion(group string) (string, uint, error) {
-	if !strings.Contains(group, "@") {
-		return group, 0, nil
-	}
-
-	envGroupSpl := strings.Split(group, "@")
-	envGroupName := envGroupSpl[0]
-	envGroupVersion := uint64(0)
-
-	envGroupVersion, err := strconv.ParseUint(envGroupSpl[1], 10, 32)
-
-	if err != nil {
-		return "", 0, err
-	}
-
-	return envGroupName, uint(envGroupVersion), nil
+	EnvGroups       []types.EnvGroupMeta
 }
 }
 
 
 func coalesceEnvGroups(
 func coalesceEnvGroups(
 	client *api.Client,
 	client *api.Client,
 	projectID, clusterID uint,
 	projectID, clusterID uint,
 	namespace string,
 	namespace string,
-	envGroups []string,
+	envGroups []types.EnvGroupMeta,
 	config map[string]interface{},
 	config map[string]interface{},
 ) error {
 ) error {
 	for _, group := range envGroups {
 	for _, group := range envGroups {
-		if group == "" {
-			continue
-		}
-
-		envGroupName, envGroupVersion, err := getEnvGroupNameVersion(group)
-
-		if err != nil {
-			return err
+		if group.Name == "" {
+			return fmt.Errorf("env group name cannot be empty")
 		}
 		}
 
 
 		envGroup, err := client.GetEnvGroup(
 		envGroup, err := client.GetEnvGroup(
@@ -64,8 +39,8 @@ func coalesceEnvGroups(
 			clusterID,
 			clusterID,
 			namespace,
 			namespace,
 			&types.GetEnvGroupRequest{
 			&types.GetEnvGroupRequest{
-				Name:    envGroupName,
-				Version: envGroupVersion,
+				Name:    group.Name,
+				Version: group.Version,
 			},
 			},
 		)
 		)
 
 

+ 78 - 3
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedJobChart.tsx

@@ -25,6 +25,7 @@ import { pushFiltered } from "../../../../shared/routing";
 import { RouteComponentProps, withRouter } from "react-router";
 import { RouteComponentProps, withRouter } from "react-router";
 import KeyValueArray from "components/form-components/KeyValueArray";
 import KeyValueArray from "components/form-components/KeyValueArray";
 import RevisionSection from "./RevisionSection";
 import RevisionSection from "./RevisionSection";
+import { onlyInLeft } from "shared/array_utils";
 
 
 type PropsType = WithAuthProps &
 type PropsType = WithAuthProps &
   RouteComponentProps & {
   RouteComponentProps & {
@@ -324,14 +325,15 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
     return ws;
     return ws;
   };
   };
 
 
-  handleSaveValues = (config?: any, runJob?: boolean) => {
+  handleSaveValues = async (config?: any, runJob?: boolean) => {
     let { currentCluster, setCurrentError, currentProject } = this.context;
     let { currentCluster, setCurrentError, currentProject } = this.context;
     this.setState({ saveValuesStatus: "loading" });
     this.setState({ saveValuesStatus: "loading" });
 
 
     let conf: string;
     let conf: string;
+    let values = {} as any;
 
 
     if (!config) {
     if (!config) {
-      let values = {};
+      values = {};
       let imageUrl = this.state.newestImage;
       let imageUrl = this.state.newestImage;
       let tag = null;
       let tag = null;
 
 
@@ -354,7 +356,7 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
       });
       });
     } else {
     } else {
       // Convert dotted keys to nested objects
       // Convert dotted keys to nested objects
-      let values = {};
+      values = {};
 
 
       for (let key in config) {
       for (let key in config) {
         _.set(values, key, config[key]);
         _.set(values, key, config[key]);
@@ -392,6 +394,79 @@ class ExpandedJobChart extends Component<PropsType, StateType> {
       );
       );
     }
     }
 
 
+    const oldSyncedEnvGroups =
+      this.props.currentChart.config?.container?.env?.synced || [];
+    const newSyncedEnvGroups = values?.container?.env?.synced || [];
+
+    const deletedEnvGroups = onlyInLeft<{
+      keys: Array<any>;
+      name: string;
+      version: number;
+    }>(
+      oldSyncedEnvGroups,
+      newSyncedEnvGroups,
+      (oldVal, newVal) => oldVal.name === newVal.name
+    );
+
+    const addedEnvGroups = onlyInLeft<{
+      keys: Array<any>;
+      name: string;
+      version: number;
+    }>(
+      newSyncedEnvGroups,
+      oldSyncedEnvGroups,
+      (oldVal, newVal) => oldVal.name === newVal.name
+    );
+
+    const addApplicationToEnvGroupPromises = addedEnvGroups.map(
+      (envGroup: any) => {
+        return api.addApplicationToEnvGroup(
+          "<token>",
+          {
+            name: envGroup?.name,
+            app_name: this.state.currentChart.name,
+          },
+          {
+            project_id: currentProject.id,
+            cluster_id: currentCluster.id,
+            namespace: this.state.currentChart.namespace,
+          }
+        );
+      }
+    );
+
+    try {
+      await Promise.all(addApplicationToEnvGroupPromises);
+    } catch (error) {
+      setCurrentError(
+        "We coudln't sync the env group to the application, please try again."
+      );
+    }
+
+    const removeApplicationToEnvGroupPromises = deletedEnvGroups.map(
+      (envGroup: any) => {
+        return api.removeApplicationFromEnvGroup(
+          "<token>",
+          {
+            name: envGroup?.name,
+            app_name: this.state.currentChart.name,
+          },
+          {
+            project_id: currentProject.id,
+            cluster_id: currentCluster.id,
+            namespace: this.state.currentChart.namespace,
+          }
+        );
+      }
+    );
+    try {
+      await Promise.all(removeApplicationToEnvGroupPromises);
+    } catch (error) {
+      setCurrentError(
+        "We coudln't remove the synced env group from the application, please try again."
+      );
+    }
+
     api
     api
       .upgradeChartValues(
       .upgradeChartValues(
         "<token>",
         "<token>",

+ 13 - 21
dashboard/src/shared/api.tsx

@@ -474,11 +474,9 @@ const detectBuildpack = baseApi<
     branch: string;
     branch: string;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
 });
 });
 
 
 const getBranchContents = baseApi<
 const getBranchContents = baseApi<
@@ -494,11 +492,9 @@ const getBranchContents = baseApi<
     branch: string;
     branch: string;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/contents`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/contents`;
 });
 });
 
 
 const getProcfileContents = baseApi<
 const getProcfileContents = baseApi<
@@ -514,11 +510,9 @@ const getProcfileContents = baseApi<
     branch: string;
     branch: string;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${
-    pathParams.git_repo_id
-  }/repos/${pathParams.kind}/${pathParams.owner}/${
-    pathParams.name
-  }/${encodeURIComponent(pathParams.branch)}/procfile`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
+    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
+    }/${encodeURIComponent(pathParams.branch)}/procfile`;
 });
 });
 
 
 const getBranches = baseApi<
 const getBranches = baseApi<
@@ -1053,7 +1047,7 @@ const listEnvGroups = baseApi<
     cluster_id: number;
     cluster_id: number;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/namespaces/${pathParams.namespace}/envgroups/list`;
+  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id}/namespaces/${pathParams.namespace}/envgroup/list`;
 });
 });
 
 
 const listConfigMaps = baseApi<
 const listConfigMaps = baseApi<
@@ -1077,11 +1071,9 @@ const getEnvGroup = baseApi<
     version?: number;
     version?: number;
   }
   }
 >("GET", (pathParams) => {
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${
-    pathParams.cluster_id
-  }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${
-    pathParams.version ? "&version=" + pathParams.version : ""
-  }`;
+  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id
+    }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : ""
+    }`;
 });
 });
 
 
 const getConfigMap = baseApi<
 const getConfigMap = baseApi<