Mohammed Nafees 3 rokov pred
rodič
commit
51c306809c

+ 3 - 16
api/server/handlers/v1/env_group/add_release.go

@@ -13,7 +13,6 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"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/models"
 )
 
@@ -44,7 +43,7 @@ func (c *AddReleaseToEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 		return
 	}
 
-	request := &types.EnvGroupReleaseRequest{}
+	request := &types.V1EnvGroupReleaseRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
 		return
@@ -61,29 +60,17 @@ func (c *AddReleaseToEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.R
 	cm, _, err := agent.GetLatestVersionedConfigMap(name, namespace)
 
 	if err != nil && errors.Is(err, kubernetes.IsNotFoundError) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("env group not found"),
-			http.StatusNotFound,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("env group not found")))
 		return
 	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	cm, err = agent.AddApplicationToVersionedConfigMap(cm, request.ReleaseName)
+	_, err = agent.AddApplicationToVersionedConfigMap(cm, request.ReleaseName)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
-
-	res, err := envgroup.ToEnvGroup(cm)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.WriteResult(w, r, res)
 }

+ 399 - 0
api/server/handlers/v1/env_group/create.go

@@ -0,0 +1,399 @@
+package env_group
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"sync"
+
+	"sigs.k8s.io/yaml"
+
+	"helm.sh/helm/v3/pkg/release"
+	v1 "k8s.io/api/core/v1"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/helm"
+	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/stacks"
+)
+
+type CreateEnvGroupHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewCreateEnvGroupHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateEnvGroupHandler {
+	return &CreateEnvGroupHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *CreateEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	request := &types.CreateEnvGroupRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	namespace := r.Context().Value(types.NamespaceScope).(string)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	agent, err := c.GetAgent(r, cluster, namespace)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	envGroup, err := envgroup.GetEnvGroup(agent, request.Name, namespace, 0)
+
+	// if the environment group exists and has MetaVersion=1, throw an error
+	if envGroup != nil && envGroup.MetaVersion == 1 {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("env group with that name already exists"),
+			http.StatusNotFound,
+		))
+
+		return
+	} else if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+	}
+
+	helmAgent, err := c.GetHelmAgent(r, cluster, namespace)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
+		Name:            request.Name,
+		Namespace:       namespace,
+		Variables:       request.Variables,
+		SecretVariables: request.SecretVariables,
+	})
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	envGroup, err = envgroup.ToEnvGroup(configMap)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	releases, err := envgroup.GetSyncedReleases(helmAgent, configMap)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := &types.V1EnvGroupResponse{
+		CreatedAt: envGroup.CreatedAt,
+		Version:   envGroup.Version,
+		Name:      envGroup.Name,
+		Releases:  envGroup.Applications,
+		Variables: envGroup.Variables,
+	}
+
+	stackId, err := stacks.GetStackForEnvGroup(c.Config(), cluster.ProjectID, cluster.ID, envGroup)
+
+	if err == nil && len(stackId) > 0 {
+		res.StackID = stackId
+	}
+
+	c.WriteResult(w, r, res)
+
+	// trigger rollout of new applications after writing the result
+	errors := rolloutApplications(c.Config(), cluster, helmAgent, envGroup, configMap, releases)
+
+	if len(errors) > 0 {
+		errStrArr := make([]string, 0)
+
+		for _, err := range errors {
+			errStrArr = append(errStrArr, err.Error())
+		}
+
+		c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(fmt.Errorf(strings.Join(errStrArr, ","))))
+		return
+	}
+
+	err = postUpgrade(c.Config(), cluster.ProjectID, cluster.ID, envGroup)
+
+	if err != nil {
+		c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+}
+
+func rolloutApplications(
+	config *config.Config,
+	cluster *models.Cluster,
+	helmAgent *helm.Agent,
+	envGroup *types.EnvGroup,
+	configMap *v1.ConfigMap,
+	releases []*release.Release,
+) []error {
+	registries, err := config.Repo.Registry().ListRegistriesByProjectID(cluster.ProjectID)
+
+	if err != nil {
+		return []error{err}
+	}
+
+	// construct the synced env section that should be written
+	newSection := &SyncedEnvSection{
+		Name:    envGroup.Name,
+		Version: envGroup.Version,
+	}
+
+	newSectionKeys := make([]SyncedEnvSectionKey, 0)
+
+	for key, val := range configMap.Data {
+		newSectionKeys = append(newSectionKeys, SyncedEnvSectionKey{
+			Name:   key,
+			Secret: strings.Contains(val, "PORTERSECRET"),
+		})
+	}
+
+	newSection.Keys = newSectionKeys
+
+	// asynchronously update releases with that image repo uri
+	var wg sync.WaitGroup
+	mu := &sync.Mutex{}
+	errors := make([]error, 0)
+
+	for i, rel := range releases {
+		index := i
+		release := rel
+		wg.Add(1)
+
+		go func() {
+			defer wg.Done()
+			// read release via agent
+			newConfig, err := getNewConfig(release.Config, newSection)
+
+			if err != nil {
+				mu.Lock()
+				errors = append(errors, err)
+				mu.Unlock()
+				return
+			}
+
+			// if this is a job chart, update the config and set correct paused param to true
+			if release.Chart.Name() == "job" {
+				newConfig["paused"] = true
+			}
+
+			conf := &helm.UpgradeReleaseConfig{
+				Name:       releases[index].Name,
+				Cluster:    cluster,
+				Repo:       config.Repo,
+				Registries: registries,
+				Values:     newConfig,
+			}
+
+			_, err = helmAgent.UpgradeReleaseByValues(conf, config.DOConf)
+
+			if err != nil {
+				mu.Lock()
+				errors = append(errors, err)
+				mu.Unlock()
+				return
+			}
+		}()
+	}
+
+	wg.Wait()
+
+	return errors
+}
+
+type SyncedEnvSection struct {
+	Name    string                `json:"name" yaml:"name"`
+	Version uint                  `json:"version" yaml:"version"`
+	Keys    []SyncedEnvSectionKey `json:"keys" yaml:"keys"`
+}
+
+type SyncedEnvSectionKey struct {
+	Name   string `json:"name" yaml:"name"`
+	Secret bool   `json:"secret" yaml:"secret"`
+}
+
+func getNewConfig(curr map[string]interface{}, syncedEnvSection *SyncedEnvSection) (map[string]interface{}, error) {
+	// look for container.env.synced
+	envConf, err := getNestedMap(curr, "container", "env")
+
+	if err != nil {
+		return nil, err
+	}
+
+	syncedEnvInter, syncedEnvExists := envConf["synced"]
+
+	if !syncedEnvExists {
+		return curr, nil
+	} else {
+		syncedArr := make([]*SyncedEnvSection, 0)
+		syncedArrInter, ok := syncedEnvInter.([]interface{})
+
+		if !ok {
+			return nil, fmt.Errorf("could not convert to synced env section: not an array")
+		}
+
+		for _, syncedArrInterObj := range syncedArrInter {
+			syncedArrObj := &SyncedEnvSection{}
+			syncedArrInterObjMap, ok := syncedArrInterObj.(map[string]interface{})
+
+			if !ok {
+				continue
+			}
+
+			if nameField, nameFieldExists := syncedArrInterObjMap["name"]; nameFieldExists {
+				syncedArrObj.Name, ok = nameField.(string)
+
+				if !ok {
+					continue
+				}
+			}
+
+			if versionField, versionFieldExists := syncedArrInterObjMap["version"]; versionFieldExists {
+				versionFloat, ok := versionField.(float64)
+
+				if !ok {
+					continue
+				}
+
+				syncedArrObj.Version = uint(versionFloat)
+			}
+
+			if keyField, keyFieldExists := syncedArrInterObjMap["keys"]; keyFieldExists {
+				keyFieldInterArr, ok := keyField.([]interface{})
+
+				if !ok {
+					continue
+				}
+
+				keyFieldMapArr := make([]map[string]interface{}, 0)
+
+				for _, keyFieldInter := range keyFieldInterArr {
+					mapConv, ok := keyFieldInter.(map[string]interface{})
+
+					if !ok {
+						continue
+					}
+
+					keyFieldMapArr = append(keyFieldMapArr, mapConv)
+				}
+
+				keyFieldRes := make([]SyncedEnvSectionKey, 0)
+
+				for _, keyFieldMap := range keyFieldMapArr {
+					toAdd := SyncedEnvSectionKey{}
+
+					if nameField, nameFieldExists := keyFieldMap["name"]; nameFieldExists {
+						toAdd.Name, ok = nameField.(string)
+
+						if !ok {
+							continue
+						}
+					}
+
+					if secretField, secretFieldExists := keyFieldMap["secret"]; secretFieldExists {
+						toAdd.Secret, ok = secretField.(bool)
+
+						if !ok {
+							continue
+						}
+					}
+
+					keyFieldRes = append(keyFieldRes, toAdd)
+				}
+
+				syncedArrObj.Keys = keyFieldRes
+			}
+
+			syncedArr = append(syncedArr, syncedArrObj)
+		}
+
+		resArr := make([]SyncedEnvSection, 0)
+		foundMatch := false
+
+		for _, candidate := range syncedArr {
+			if candidate.Name == syncedEnvSection.Name {
+				resArr = append(resArr, *syncedEnvSection)
+				foundMatch = true
+			} else {
+				resArr = append(resArr, *candidate)
+			}
+		}
+
+		if !foundMatch {
+			return curr, nil
+		}
+
+		envConf["synced"] = resArr
+	}
+
+	// to remove all types that Helm may not be able to work with, we marshal to and from
+	// yaml for good measure. Otherwise we get silly error messages like:
+	// Upgrade failed: template: web/templates/deployment.yaml:138:40: executing \"web/templates/deployment.yaml\"
+	// at <$syncedEnv.keys>: can't evaluate field keys in type namespace.SyncedEnvSection
+	currYAML, err := yaml.Marshal(curr)
+
+	if err != nil {
+		return nil, err
+	}
+
+	res := make(map[string]interface{})
+
+	err = yaml.Unmarshal([]byte(currYAML), &res)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return res, nil
+}
+
+func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
+	var res map[string]interface{}
+	curr := obj
+
+	for _, field := range fields {
+		objField, ok := curr[field]
+
+		if !ok {
+			return nil, fmt.Errorf("%s not found", field)
+		}
+
+		res, ok = objField.(map[string]interface{})
+
+		if !ok {
+			return nil, fmt.Errorf("%s is not a nested object", field)
+		}
+
+		curr = res
+	}
+
+	return res, nil
+}
+
+// postUpgrade runs any necessary scripting after the release has been upgraded.
+func postUpgrade(config *config.Config, projectID, clusterID uint, envGroup *types.EnvGroup) error {
+	// update the relevant env group version number if tied to a stack resource
+	return stacks.UpdateEnvGroupVersion(config, projectID, clusterID, envGroup)
+}

+ 2 - 2
api/server/handlers/v1/env_group/delete.go

@@ -66,8 +66,8 @@ func (c *DeleteEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	} else if envGroup != nil && envGroup.MetaVersion == 2 {
 		if len(envGroup.Applications) != 0 {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("env group must not have any connected applications"),
-				http.StatusNotFound,
+				fmt.Errorf("env group must not have any connected releases"),
+				http.StatusPreconditionFailed,
 			))
 
 			return

+ 12 - 19
api/server/handlers/v1/env_group/get.go

@@ -1,7 +1,6 @@
 package env_group
 
 import (
-	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -16,7 +15,6 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/stacks"
-	"gorm.io/gorm"
 )
 
 type GetEnvGroupHandler struct {
@@ -64,31 +62,26 @@ func (c *GetEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		if strings.Contains(err.Error(), "not found") {
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("env group not found"),
-				http.StatusNotFound),
-			)
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("env group not found")))
 			return
 		}
+
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	stackId, err := stacks.GetStackForEnvGroup(c.Config(), cluster.ProjectID, cluster.ID, envGroup)
-
-	if err != nil {
-		if errors.Is(err, gorm.ErrRecordNotFound) {
-			c.WriteResult(w, r, &types.GetEnvGroupResponse{EnvGroup: envGroup})
-			return
-		}
-
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
+	res := &types.V1EnvGroupResponse{
+		CreatedAt: envGroup.CreatedAt,
+		Version:   envGroup.Version,
+		Name:      envGroup.Name,
+		Releases:  envGroup.Applications,
+		Variables: envGroup.Variables,
 	}
 
-	res := &types.GetEnvGroupResponse{
-		EnvGroup: envGroup,
-		StackID:  stackId,
+	stackId, err := stacks.GetStackForEnvGroup(c.Config(), cluster.ProjectID, cluster.ID, envGroup)
+
+	if err == nil && len(stackId) > 0 {
+		res.StackID = stackId
 	}
 
 	c.WriteResult(w, r, res)

+ 16 - 5
api/server/handlers/v1/env_group/get_all_versions.go

@@ -15,6 +15,7 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/stacks"
 )
 
 type GetEnvGroupAllVersionsHandler struct {
@@ -64,7 +65,7 @@ func (c *GetEnvGroupAllVersionsHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
-	res := make(types.ListEnvGroupsResponse, 0)
+	var res types.V1EnvGroupsAllVersionsResponse
 
 	for _, cm := range configMaps {
 		eg, err := envgroup.ToEnvGroup(&cm)
@@ -73,11 +74,21 @@ func (c *GetEnvGroupAllVersionsHandler) ServeHTTP(w http.ResponseWriter, r *http
 			continue
 		}
 
-		res = append(res, &types.EnvGroupMeta{
-			Name:      eg.Name,
-			Namespace: eg.Namespace,
+		elem := &types.V1EnvGroupResponse{
+			CreatedAt: eg.CreatedAt,
 			Version:   eg.Version,
-		})
+			Name:      eg.Name,
+			Releases:  eg.Applications,
+			Variables: eg.Variables,
+		}
+
+		stackId, err := stacks.GetStackForEnvGroup(c.Config(), cluster.ProjectID, cluster.ID, eg)
+
+		if err == nil && len(stackId) > 0 {
+			elem.StackID = stackId
+		}
+
+		res = append(res, elem)
 	}
 
 	c.WriteResult(w, r, res)

+ 104 - 0
api/server/handlers/v1/env_group/list.go

@@ -0,0 +1,104 @@
+package env_group
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/stacks"
+)
+
+type ListEnvGroupsHandler struct {
+	handlers.PorterHandlerWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewListEnvGroupsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListEnvGroupsHandler {
+	return &ListEnvGroupsHandler{
+		PorterHandlerWriter:   handlers.NewDefaultPorterHandler(config, nil, writer),
+		KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *ListEnvGroupsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	namespace := r.Context().Value(types.NamespaceScope).(string)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	agent, err := c.GetAgent(r, cluster, "")
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	// get all versioned config maps
+	configMaps, err := agent.ListAllVersionedConfigMaps(namespace)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.V1ListAllEnvGroupsResponse
+
+	for _, cm := range configMaps {
+		eg, err := envgroup.ToEnvGroup(&cm)
+
+		if err != nil {
+			continue
+		}
+
+		elem := &types.V1EnvGroupMeta{
+			CreatedAt: eg.CreatedAt,
+			Name:      eg.Name,
+		}
+
+		stackId, err := stacks.GetStackForEnvGroup(c.Config(), cluster.ProjectID, cluster.ID, eg)
+
+		if err == nil && len(stackId) > 0 {
+			elem.StackID = stackId
+		}
+
+		res = append(res, elem)
+	}
+
+	// get all meta-version 1 configmaps
+	configMapList, err := agent.ListConfigMaps(namespace)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	for _, v1CM := range configMapList.Items {
+		eg, err := envgroup.ToEnvGroup(&v1CM)
+
+		if err != nil {
+			continue
+		}
+
+		elem := &types.V1EnvGroupMeta{
+			CreatedAt: eg.CreatedAt,
+			Name:      eg.Name,
+		}
+
+		stackId, err := stacks.GetStackForEnvGroup(c.Config(), cluster.ProjectID, cluster.ID, eg)
+
+		if err == nil && len(stackId) > 0 {
+			elem.StackID = stackId
+		}
+
+		res = append(res, elem)
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 4 - 20
api/server/handlers/v1/env_group/remove_release.go

@@ -13,7 +13,6 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"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/models"
 )
 
@@ -44,7 +43,7 @@ func (c *RemoveReleaseFromEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *h
 		return
 	}
 
-	request := &types.EnvGroupReleaseRequest{}
+	request := &types.V1EnvGroupReleaseRequest{}
 
 	if ok := c.DecodeAndValidate(w, r, request); !ok {
 		return
@@ -61,35 +60,20 @@ func (c *RemoveReleaseFromEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *h
 	cm, _, err := agent.GetLatestVersionedConfigMap(name, namespace)
 
 	if err != nil && errors.Is(err, kubernetes.IsNotFoundError) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("env group not found"),
-			http.StatusNotFound,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("env group not found")))
 		return
 	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	cm, err = agent.RemoveApplicationFromVersionedConfigMap(cm, request.ReleaseName)
+	_, err = agent.RemoveApplicationFromVersionedConfigMap(cm, request.ReleaseName)
 
 	if err != nil && errors.Is(err, kubernetes.IsNotFoundError) {
-		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-			fmt.Errorf("env group not found"),
-			http.StatusNotFound,
-		))
+		c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("env group not found")))
 		return
 	} else if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
-
-	res, err := envgroup.ToEnvGroup(cm)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	c.WriteResult(w, r, res)
 }

+ 162 - 12
api/server/router/v1/env_group.go

@@ -4,7 +4,6 @@ import (
 	"fmt"
 
 	"github.com/go-chi/chi"
-	"github.com/porter-dev/porter/api/server/handlers/namespace"
 	v1EnvGroup "github.com/porter-dev/porter/api/server/handlers/v1/env_group"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/config"
@@ -13,7 +12,7 @@ import (
 )
 
 // swagger:parameters getEnvGroup
-type envGroupPathParams struct {
+type envGroupVersionPathParams struct {
 	// The project id
 	// in: path
 	// required: true
@@ -43,6 +42,31 @@ type envGroupPathParams struct {
 	Version uint `json:"version"`
 }
 
+// swagger:parameters getEnvGroupAllVersions deleteEnvGroup addReleaseToEnvGroup removeReleaseFromEnvGroup
+type envGroupPathParams struct {
+	// The project id
+	// in: path
+	// required: true
+	// minimum: 1
+	ProjectID uint `json:"project_id"`
+
+	// The cluster id
+	// in: path
+	// required: true
+	// minimum: 1
+	ClusterID uint `json:"cluster_id"`
+
+	// The namespace name
+	// in: path
+	// required: true
+	Namespace string `json:"namespace"`
+
+	// The env group name
+	// in: path
+	// required: true
+	Name string `json:"name"`
+}
+
 func NewV1EnvGroupScopedRegisterer(children ...*router.Registerer) *router.Registerer {
 	return &router.Registerer{
 		GetRoutes: GetV1EnvGroupScopedRoutes,
@@ -111,10 +135,10 @@ func getV1EnvGroupRoutes(
 	//     schema:
 	//       $ref: '#/definitions/CreateEnvGroupRequest'
 	// responses:
-	//   '201':
+	//   '200':
 	//     description: Successfully created a new namespace
 	//     schema:
-	//       $ref: '#/definitions/NamespaceResponse'
+	//       $ref: '#/definitions/V1EnvGroupResponse'
 	//   '403':
 	//     description: Forbidden
 	createOrUpdateEnvGroupEndpoint := factory.NewAPIEndpoint(
@@ -134,7 +158,7 @@ func getV1EnvGroupRoutes(
 		},
 	)
 
-	createOrUpdateEnvGroupHandler := namespace.NewCreateEnvGroupHandler(
+	createOrUpdateEnvGroupHandler := v1EnvGroup.NewCreateEnvGroupHandler(
 		config,
 		factory.GetDecoderValidator(),
 		factory.GetResultWriter(),
@@ -150,9 +174,7 @@ func getV1EnvGroupRoutes(
 	// swagger:operation GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name}/versions/{version} getEnvGroup
 	//
 	// Gets an env group denoted by `name` and `version` in the namespace denoted by `namespace`. The namespace should belong to the cluster denoted by
-	// `cluster_id`. The cluster should belong to the project denoted by `project_id`.
-	//
-	// **Note:** To get the latest version of an env group, set `version` to `0`.
+	// `cluster_id`, which in turn should belong to the project denoted by `project_id`. **Note:** To get the latest version of an env group, set `version` to `0`.
 	//
 	// ---
 	// produces:
@@ -167,12 +189,14 @@ func getV1EnvGroupRoutes(
 	//   - name: name
 	//   - name: version
 	// responses:
-	//   '201':
-	//     description: Successfully created a new namespace
+	//   '200':
+	//     description: Successfully fetched the env group
 	//     schema:
-	//       $ref: '#/definitions/NamespaceResponse'
+	//       $ref: '#/definitions/V1EnvGroupResponse'
 	//   '403':
 	//     description: Forbidden
+	//   '404':
+	//     description: Env group not found
 	getEnvGroupEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
@@ -204,6 +228,31 @@ func getV1EnvGroupRoutes(
 	})
 
 	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name} -> env_group.NewGetEnvGroupAllVersionsHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name} getEnvGroupAllVersions
+	//
+	// Gets all versions of an env group denoted by `name` in the namespace denoted by `namespace`. The namespace should belong to the cluster denoted by
+	// `cluster_id`, which in turn should belong to the project denoted by `project_id`.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Get all versions of an env group
+	// tags:
+	// - Env groups
+	// parameters:
+	//   - name: project_id
+	//   - name: cluster_id
+	//   - name: namespace
+	//   - name: name
+	// responses:
+	//   '200':
+	//     description: Successfully fetched the env group
+	//     schema:
+	//       $ref: '#/definitions/V1EnvGroupsAllVersionsResponse'
+	//   '403':
+	//     description: Forbidden
+	//   '404':
+	//     description: Env group not found
 	getEnvGroupAllVersionsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
@@ -234,6 +283,28 @@ func getV1EnvGroupRoutes(
 	})
 
 	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups -> namespace.NewListEnvGroupsHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups listAllEnvGroups
+	//
+	// Gets all env groups in the namespace denoted by `namespace`. The namespace should belong to the cluster denoted by
+	// `cluster_id`, which in turn should belong to the project denoted by `project_id`.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Get all env groups
+	// tags:
+	// - Env groups
+	// parameters:
+	//   - name: project_id
+	//   - name: cluster_id
+	//   - name: namespace
+	// responses:
+	//   '200':
+	//     description: Successfully fetched the env group
+	//     schema:
+	//       $ref: '#/definitions/V1ListAllEnvGroupsResponse'
+	//   '403':
+	//     description: Forbidden
 	listEnvGroupsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
@@ -251,7 +322,7 @@ func getV1EnvGroupRoutes(
 		},
 	)
 
-	listEnvGroupsHandler := namespace.NewListEnvGroupsHandler(
+	listEnvGroupsHandler := v1EnvGroup.NewListEnvGroupsHandler(
 		config,
 		factory.GetResultWriter(),
 	)
@@ -263,6 +334,29 @@ func getV1EnvGroupRoutes(
 	})
 
 	// DELETE /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name} -> env_group.NewDeleteEnvGroupHandler
+	// swagger:operation DELETE /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name} deleteEnvGroup
+	//
+	// Deletes the env group denoted by `name` in the namespace denoted by `namespace`. The namespace should belong to the cluster denoted by
+	// `cluster_id`, which in turn should belong to the project denoted by `project_id`.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Delete an env group
+	// tags:
+	// - Env groups
+	// parameters:
+	//   - name: project_id
+	//   - name: cluster_id
+	//   - name: namespace
+	//   - name: name
+	// responses:
+	//   '200':
+	//     description: Successfully deleted the env group
+	//   '403':
+	//     description: Forbidden
+	//   '412':
+	//     description: Env group is linked to one or more releases
 	deleteEnvGroupEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbDelete,
@@ -293,6 +387,34 @@ func getV1EnvGroupRoutes(
 	})
 
 	// PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name}/add_release -> env_group.NewAddReleaseToEnvGroupHandler
+	// swagger:operation PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name}/add_release addReleaseToEnvGroup
+	//
+	// Adds a release to the env group denoted by `name` in the namespace denoted by `namespace`. The namespace should belong to the cluster denoted by
+	// `cluster_id`, which in turn should belong to the project denoted by `project_id`.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Add a release to an env group
+	// tags:
+	// - Env groups
+	// parameters:
+	//   - name: project_id
+	//   - name: cluster_id
+	//   - name: namespace
+	//   - name: name
+	//   - in: body
+	//     name: V1EnvGroupReleaseRequest
+	//     description: The name of the release to add
+	//     schema:
+	//       $ref: '#/definitions/V1EnvGroupReleaseRequest'
+	// responses:
+	//   '200':
+	//     description: Successfully added the release
+	//   '403':
+	//     description: Forbidden
+	//   '404':
+	//     description: Env group not found
 	addReleaseToEnvGroupEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbUpdate,
@@ -323,6 +445,34 @@ func getV1EnvGroupRoutes(
 	})
 
 	// PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name}/remove_release -> env_group.NewRemoveReleaseFromEnvGroupHandler
+	// swagger:operation PATCH /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/env_groups/{name}/remove_release removeReleaseFromEnvGroup
+	//
+	// Removes a release from the env group denoted by `name` in the namespace denoted by `namespace`. The namespace should belong to the cluster denoted by
+	// `cluster_id`, which in turn should belong to the project denoted by `project_id`.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: Remove a release from an env group
+	// tags:
+	// - Env groups
+	// parameters:
+	//   - name: project_id
+	//   - name: cluster_id
+	//   - name: namespace
+	//   - name: name
+	//   - in: body
+	//     name: V1EnvGroupReleaseRequest
+	//     description: The name of the release to remove
+	//     schema:
+	//       $ref: '#/definitions/V1EnvGroupReleaseRequest'
+	// responses:
+	//   '200':
+	//     description: Successfully removed the release
+	//   '403':
+	//     description: Forbidden
+	//   '404':
+	//     description: Env group not found
 	removeReleaseFromEnvGroupEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbUpdate,

+ 1 - 1
api/server/router/v1/namespace.go

@@ -8,7 +8,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 )
 
-// swagger:parameters getNamespace deleteNamespace createRelease createStack listStacks createOrUpdateEnvGroup
+// swagger:parameters getNamespace deleteNamespace createRelease createStack listStacks createOrUpdateEnvGroup listAllEnvGroups
 type namespacePathParams struct {
 	// The project id
 	// in: path

+ 46 - 8
api/types/namespace.go

@@ -111,7 +111,6 @@ type CreateConfigMapRequest struct {
 }
 
 type EnvGroup struct {
-	//
 	MetaVersion  uint              `json:"meta_version"`
 	CreatedAt    time.Time         `json:"created_at"`
 	Version      uint              `json:"version"`
@@ -223,19 +222,58 @@ type StreamJobRunsRequest struct {
 	Name string `schema:"name"`
 }
 
-// GetEnvGroupResponse represents the response body containing an env group
-//
-// swagger: model
 type GetEnvGroupResponse struct {
 	*EnvGroup
-
-	// the stack ID of the stack containing this env group (if any)
 	StackID string `json:"stack_id,omitempty"`
 }
 
-// CreateEnvGroupRequest represents the request body to create or update an env group
+// V1EnvGroupReleaseRequest represents the request body to add or remove a release in an env group
 //
 // swagger:model
-type EnvGroupReleaseRequest struct {
+type V1EnvGroupReleaseRequest struct {
 	ReleaseName string `json:"release_name" form:"required"`
 }
+
+// V1EnvGroupResponse defines an env group
+//
+// swagger:model
+type V1EnvGroupResponse struct {
+	// the UTC timestamp in RFC 3339 format indicating the creation time of the env group
+	CreatedAt time.Time `json:"created_at"`
+
+	// the version of the env group
+	Version uint `json:"version"`
+
+	// the name of the env group
+	Name string `json:"name"`
+
+	// the list of releases linked to this env group
+	Releases []string `json:"releases"`
+
+	// the variables contained in this env group
+	Variables map[string]string `json:"variables"`
+
+	// the ID of the stack containing this env group (if any)
+	StackID string `json:"stack_id,omitempty"`
+}
+
+// V1EnvGroupsAllVersionsResponse represents the response body containing all versions of an env group
+//
+// swagger:model
+type V1EnvGroupsAllVersionsResponse []*V1EnvGroupResponse
+
+type V1EnvGroupMeta struct {
+	// the UTC timestamp in RFC 3339 format indicating the creation time of the env group
+	CreatedAt time.Time `json:"created_at"`
+
+	// the name of the env group
+	Name string `json:"name"`
+
+	// the ID of the stack containing this env group (if any)
+	StackID string `json:"stack_id,omitempty"`
+}
+
+// V1ListAllEnvGroupsResponse represents the response body containing the list of env groups
+//
+// swagger:model
+type V1ListAllEnvGroupsResponse []*V1EnvGroupMeta