Преглед изворни кода

initial stacks backend with env group support

Alexander Belanger пре 3 година
родитељ
комит
5a705ce095

+ 14 - 0
api/server/handlers/namespace/create_env_group.go

@@ -20,6 +20,7 @@ import (
 	"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 {
@@ -115,6 +116,13 @@ func (c *CreateEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		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(
@@ -367,3 +375,9 @@ func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]inte
 
 	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)
+}

+ 112 - 44
api/server/handlers/stack/create.go

@@ -13,6 +13,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/encryption"
+	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/models"
 
 	helmrelease "helm.sh/helm/v3/pkg/release"
@@ -73,6 +74,13 @@ func (p *StackCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	envGroups, err := getEnvGroupModels(req.EnvGroups, proj.ID, cluster.ID, namespace)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	// write stack to the database with creating status
 	stack := &models.Stack{
 		ProjectID: proj.ID,
@@ -86,6 +94,7 @@ func (p *StackCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 				Status:         string(types.StackRevisionStatusDeploying),
 				SourceConfigs:  sourceConfigs,
 				Resources:      resources,
+				EnvGroups:      envGroups,
 			},
 		},
 	}
@@ -97,76 +106,88 @@ func (p *StackCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// apply all app resources
-	registries, err := p.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
-
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	helmAgent, err := p.GetHelmAgent(r, cluster, "")
+	// apply all env groups
+	k8sAgent, err := p.GetAgent(r, cluster, "")
 
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 		return
 	}
 
-	helmReleaseMap := make(map[string]*helmrelease.Release)
+	envGroupDeployErrors := make([]string, 0)
 
-	deployErrs := make([]string, 0)
-
-	for _, appResource := range req.AppResources {
-		rel, err := applyAppResource(&applyAppResourceOpts{
-			config:     p.Config(),
-			projectID:  proj.ID,
-			namespace:  namespace,
-			cluster:    cluster,
-			registries: registries,
-			helmAgent:  helmAgent,
-			request:    appResource,
+	for _, envGroup := range req.EnvGroups {
+		_, err := envgroup.CreateEnvGroup(k8sAgent, types.ConfigMapInput{
+			Name:            envGroup.Name,
+			Namespace:       namespace,
+			Variables:       envGroup.Variables,
+			SecretVariables: envGroup.SecretVariables,
 		})
 
 		if err != nil {
-			deployErrs = append(deployErrs, err.Error())
-		} else {
-			helmReleaseMap[fmt.Sprintf("%s/%s", namespace, appResource.Name)] = rel
+			envGroupDeployErrors = append(envGroupDeployErrors, fmt.Sprintf("error creating env group %s", envGroup.Name))
 		}
 	}
 
-	// update stack revision status
 	revision := &stack.Revisions[0]
 
-	if len(deployErrs) > 0 {
+	if len(envGroupDeployErrors) > 0 {
 		revision.Status = string(types.StackRevisionStatusFailed)
-		revision.Reason = "DeployError"
-		revision.Message = strings.Join(deployErrs, " , ")
+		revision.Reason = "EnvGroupDeployErr"
+		revision.Message = strings.Join(envGroupDeployErrors, " , ")
+
+		revision, err = p.Repo().Stack().UpdateStackRevision(revision)
+
+		if err != nil {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 	} else {
-		revision.Status = string(types.StackRevisionStatusDeployed)
-	}
+		// apply all app resources
+		registries, err := p.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
+
+		if err != nil {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
 
-	revision, err = p.Repo().Stack().UpdateStackRevision(revision)
+		helmAgent, err := p.GetHelmAgent(r, cluster, "")
 
-	if err != nil {
-		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
+		if err != nil {
+			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		helmReleaseMap := make(map[string]*helmrelease.Release)
 
-	saveErrs := make([]string, 0)
+		deployErrs := make([]string, 0)
 
-	for _, resource := range revision.Resources {
-		if rel, exists := helmReleaseMap[fmt.Sprintf("%s/%s", namespace, resource.Name)]; exists {
-			_, err = release.CreateAppReleaseFromHelmRelease(p.Config(), proj.ID, cluster.ID, resource.ID, rel)
+		for _, appResource := range req.AppResources {
+			rel, err := applyAppResource(&applyAppResourceOpts{
+				config:     p.Config(),
+				projectID:  proj.ID,
+				namespace:  namespace,
+				cluster:    cluster,
+				registries: registries,
+				helmAgent:  helmAgent,
+				request:    appResource,
+			})
 
 			if err != nil {
-				saveErrs = append(saveErrs, fmt.Sprintf("the resource %s/%s could not be saved right now", namespace, resource.Name))
+				deployErrs = append(deployErrs, err.Error())
+			} else {
+				helmReleaseMap[fmt.Sprintf("%s/%s", namespace, appResource.Name)] = rel
 			}
 		}
-	}
 
-	if len(saveErrs) > 0 {
-		revision.Reason = "SaveError"
-		revision.Message = strings.Join(saveErrs, " , ")
+		// update stack revision status
+		if len(deployErrs) > 0 {
+			revision.Status = string(types.StackRevisionStatusFailed)
+			revision.Reason = "DeployError"
+			revision.Message = strings.Join(deployErrs, " , ")
+		} else {
+			revision.Status = string(types.StackRevisionStatusDeployed)
+		}
 
 		revision, err = p.Repo().Stack().UpdateStackRevision(revision)
 
@@ -174,6 +195,30 @@ func (p *StackCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
 			return
 		}
+
+		saveErrs := make([]string, 0)
+
+		for _, resource := range revision.Resources {
+			if rel, exists := helmReleaseMap[fmt.Sprintf("%s/%s", namespace, resource.Name)]; exists {
+				_, err = release.CreateAppReleaseFromHelmRelease(p.Config(), proj.ID, cluster.ID, resource.ID, rel)
+
+				if err != nil {
+					saveErrs = append(saveErrs, fmt.Sprintf("the resource %s/%s could not be saved right now", namespace, resource.Name))
+				}
+			}
+		}
+
+		if len(saveErrs) > 0 {
+			revision.Reason = "SaveError"
+			revision.Message = strings.Join(saveErrs, " , ")
+
+			revision, err = p.Repo().Stack().UpdateStackRevision(revision)
+
+			if err != nil {
+				p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				return
+			}
+		}
 	}
 
 	// read the stack again to get the latest revision info
@@ -248,3 +293,26 @@ func getResourceModels(appResources []*types.CreateStackAppResourceRequest, sour
 
 	return res, nil
 }
+
+func getEnvGroupModels(envGroups []*types.CreateStackEnvGroupRequest, projID, clusterID uint, namespace string) ([]models.StackEnvGroup, error) {
+	res := make([]models.StackEnvGroup, 0)
+
+	for _, envGroup := range envGroups {
+		uid, err := encryption.GenerateRandomBytes(16)
+
+		if err != nil {
+			return nil, err
+		}
+
+		res = append(res, models.StackEnvGroup{
+			Name:            envGroup.Name,
+			UID:             uid,
+			EnvGroupVersion: 1,
+			ProjectID:       projID,
+			ClusterID:       clusterID,
+			Namespace:       namespace,
+		})
+	}
+
+	return res, nil
+}

+ 36 - 0
api/server/handlers/stack/list_revisions.go

@@ -0,0 +1,36 @@
+package stack
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type StackListRevisionsHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewStackListRevisionsHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *StackListRevisionsHandler {
+	return &StackListRevisionsHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *StackListRevisionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	stack, _ := r.Context().Value(types.StackScope).(*models.Stack)
+
+	res := make([]types.StackRevision, 0)
+
+	for _, stackRev := range stack.Revisions {
+		res = append(res, *stackRev.ToStackRevisionType(stack.UID))
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 8 - 0
api/server/handlers/stack/rollback.go

@@ -86,8 +86,16 @@ func (p *StackRollbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	envGroups, err := stacks.CloneEnvGroups(revision.EnvGroups)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	revision.SourceConfigs = newSourceConfigs
 	revision.Resources = appResources
+	revision.EnvGroups = envGroups
 
 	revision, err = p.Repo().Stack().AppendNewRevision(revision)
 

+ 57 - 5
api/server/router/v1/stack.go

@@ -58,11 +58,11 @@ type stackRevisionPathParams struct {
 	// required: true
 	StackID string `json:"stack_id"`
 
-	// The stack revision number
+	// The stack revision id
 	// in: path
 	// required: true
 	// minimum: 1
-	StackRevisionNumber string `json:"stack_revision_number"`
+	RevisionID string `json:"revision_id"`
 }
 
 func NewV1StackScopedRegisterer(children ...*router.Registerer) *router.Registerer {
@@ -267,8 +267,60 @@ func getV1StackRoutes(
 		Router:   r,
 	})
 
-	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id}/{stack_revision_number} -> stack.NewStackGetRevisionHandler
-	// swagger:operation GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id}/{stack_revision_number} getStackRevision
+	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id}/revisions -> stack.NewStackListRevisionsHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id}/revisions listStackRevisions
+	//
+	// Lists revisions in a stack. A max of 100 revisions will be returned, sorted from most recent to least recent.
+	//
+	// ---
+	// produces:
+	// - application/json
+	// summary: List stack revisions
+	// tags:
+	// - Stacks
+	// parameters:
+	//   - name: project_id
+	//   - name: cluster_id
+	//   - name: namespace
+	//   - name: stack_id
+	// responses:
+	//   '200':
+	//     description: Successfully listed stack revisions
+	//     schema:
+	//       $ref: '#/definitions/ListStackRevisionsResponse'
+	//   '403':
+	//     description: Forbidden
+	listRevisionsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/{stack_id}/revisions",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+				types.StackScope,
+			},
+		},
+	)
+
+	listRevisionsHandler := stack.NewStackListRevisionsHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: listRevisionsEndpoint,
+		Handler:  listRevisionsHandler,
+		Router:   r,
+	})
+
+	// GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id}/{revision_id} -> stack.NewStackGetRevisionHandler
+	// swagger:operation GET /api/v1/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/stacks/{stack_id}/{revision_id} getStackRevision
 	//
 	// Gets a stack revision
 	//
@@ -283,7 +335,7 @@ func getV1StackRoutes(
 	//   - name: cluster_id
 	//   - name: namespace
 	//   - name: stack_id
-	//   - name: stack_revision_number
+	//   - name: revision_id
 	// responses:
 	//   '200':
 	//     description: Successfully got the stack revision

+ 54 - 1
api/types/stacks.go

@@ -17,6 +17,9 @@ type CreateStackRequest struct {
 	// registry or linked to a remote Git repository.
 	// required: true
 	SourceConfigs []*CreateStackSourceConfigRequest `json:"source_configs,omitempty" form:"required,dive,required"`
+
+	// A list of env groups which can be synced to an application
+	EnvGroups []*CreateStackEnvGroupRequest `json:"env_groups,omitempty" form:"required,dive,required"`
 }
 
 // swagger:model
@@ -84,6 +87,9 @@ type Stack struct {
 	Revisions []StackRevisionMeta `json:"revisions,omitempty"`
 }
 
+// swagger:model
+type ListStackRevisionsResponse []StackRevision
+
 // swagger:model
 type StackListResponse []Stack
 
@@ -109,7 +115,7 @@ type StackResource struct {
 	// If this is an app resource, app-specific information for the resource
 	StackAppData *StackResourceAppData `json:"stack_app_data,omitempty"`
 
-	// The source configuration for this stack
+	// The source configuration that this resource uses, if this is an application resource
 	StackSourceConfig *StackSourceConfig `json:"stack_source_config,omitempty"`
 }
 
@@ -158,7 +164,34 @@ type StackRevision struct {
 	// The list of resources deployed in this revision
 	Resources []StackResource `json:"resources"`
 
+	// The list of source configs deployed in this revision
 	SourceConfigs []StackSourceConfig `json:"source_configs"`
+
+	// The list of env groups scoped to this stack
+	EnvGroups []StackEnvGroup `json:"env_groups"`
+}
+
+type StackEnvGroup struct {
+	// The time that this resource was initially created
+	CreatedAt time.Time `json:"created_at"`
+
+	// The time that this resource was last updated
+	UpdatedAt time.Time `json:"updated_at"`
+
+	// The stack ID that this resource belongs to
+	StackID string `json:"stack_id"`
+
+	// The numerical revision id that this resource belongs to
+	StackRevisionID uint `json:"stack_revision_id"`
+
+	// The name of the resource
+	Name string `json:"name"`
+
+	// The id for this resource
+	ID string `json:"id"`
+
+	// The version of the env group which is being used
+	EnvGroupVersion uint `json:"env_group_version"`
 }
 
 type StackSourceConfig struct {
@@ -190,6 +223,26 @@ type StackSourceConfig struct {
 	StackSourceConfigBuild *StackSourceConfigBuild `json:"build,omitempty"`
 }
 
+// swagger:model
+type CreateStackEnvGroupRequest struct {
+	// The name of the env group
+	// required: true
+	Name string `json:"name" form:"required"`
+
+	// The non-secret variables to set in the env group
+	// required: true
+	Variables map[string]string `json:"variables,required" form:"required"`
+
+	// The secret variables to set in the env group
+	// required: true
+	SecretVariables map[string]string `json:"secret_variables,required" form:"required"`
+
+	// The list of applications that this env group should be synced to. These applications **must** be present
+	// in the stack - if an env group is created from a stack, syncing to applications which are not in the stack
+	// is not supported
+	LinkedApplications []string `json:"linked_applications"`
+}
+
 // swagger:model
 type CreateStackSourceConfigRequest struct {
 	// required: true

+ 39 - 0
internal/models/stack.go

@@ -62,6 +62,8 @@ type StackRevision struct {
 	Resources []StackResource
 
 	SourceConfigs []StackSourceConfig
+
+	EnvGroups []StackEnvGroup
 }
 
 func (s StackRevision) ToStackRevisionMetaType(stackID string) types.StackRevisionMeta {
@@ -88,10 +90,17 @@ func (s StackRevision) ToStackRevisionType(stackID string) *types.StackRevision
 		resources = append(resources, *stackResource.ToStackResource(stackID, s.RevisionNumber, s.SourceConfigs))
 	}
 
+	envGroups := make([]types.StackEnvGroup, 0)
+
+	for _, stackEnvGroup := range s.EnvGroups {
+		envGroups = append(envGroups, *stackEnvGroup.ToStackEnvGroupType(stackID, s.RevisionNumber))
+	}
+
 	return &types.StackRevision{
 		StackRevisionMeta: &metaType,
 		SourceConfigs:     sourceConfigs,
 		Resources:         resources,
+		EnvGroups:         envGroups,
 		Reason:            s.Reason,
 		Message:           s.Message,
 	}
@@ -176,3 +185,33 @@ func (s StackSourceConfig) ToStackSourceConfigType(stackID string, stackRevision
 		ImageTag:        s.ImageTag,
 	}
 }
+
+type StackEnvGroup struct {
+	gorm.Model
+
+	StackRevisionID uint
+
+	Name string
+
+	Namespace string
+
+	ProjectID uint
+
+	ClusterID uint
+
+	UID string
+
+	EnvGroupVersion uint
+}
+
+func (s StackEnvGroup) ToStackEnvGroupType(stackID string, stackRevisionID uint) *types.StackEnvGroup {
+	return &types.StackEnvGroup{
+		CreatedAt:       s.CreatedAt,
+		UpdatedAt:       s.UpdatedAt,
+		StackID:         stackID,
+		StackRevisionID: stackRevisionID,
+		Name:            s.Name,
+		ID:              s.UID,
+		EnvGroupVersion: s.EnvGroupVersion,
+	}
+}

+ 1 - 0
internal/repository/gorm/migrate.go

@@ -55,6 +55,7 @@ func AutoMigrate(db *gorm.DB, debug bool) error {
 		&models.StackRevision{},
 		&models.StackResource{},
 		&models.StackSourceConfig{},
+		&models.StackEnvGroup{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 15 - 3
internal/repository/gorm/stack.go

@@ -49,7 +49,7 @@ func (repo *StackRepository) ListStacks(projectID, clusterID uint, namespace str
 	// query for each stack's revision
 	revisions := make([]*models.StackRevision, 0)
 
-	if err := repo.db.Preload("SourceConfigs").Preload("Resources").Where("stack_revisions.stack_id IN (?)", stackIDs).Where(`
+	if err := repo.db.Preload("SourceConfigs").Preload("Resources").Preload("EnvGroups").Where("stack_revisions.stack_id IN (?)", stackIDs).Where(`
 	stack_revisions.id IN (
 	  SELECT s2.id FROM (SELECT MAX(stack_revisions.id) id FROM stack_revisions WHERE stack_revisions.stack_id IN (?) GROUP BY stack_revisions.stack_id) s2
 	)
@@ -83,6 +83,7 @@ func (repo *StackRepository) ReadStackByID(projectID, stackID uint) (*models.Sta
 		}).
 		Preload("Revisions.Resources").
 		Preload("Revisions.SourceConfigs").
+		Preload("Revisions.EnvGroups").
 		Where("stacks.project_id = ? AND stacks.id = ?", projectID, stackID).First(&stack).Error; err != nil {
 		return nil, err
 	}
@@ -100,6 +101,7 @@ func (repo *StackRepository) ReadStackByStringID(projectID uint, stackID string)
 		}).
 		Preload("Revisions.Resources").
 		Preload("Revisions.SourceConfigs").
+		Preload("Revisions.EnvGroups").
 		Where("stacks.project_id = ? AND stacks.uid = ?", projectID, stackID).First(&stack).Error; err != nil {
 		return nil, err
 	}
@@ -127,7 +129,7 @@ func (repo *StackRepository) UpdateStackRevision(revision *models.StackRevision)
 func (repo *StackRepository) ReadStackRevision(stackRevisionID uint) (*models.StackRevision, error) {
 	revision := &models.StackRevision{}
 
-	if err := repo.db.Preload("Resources").Preload("SourceConfigs").Where("id = ?", stackRevisionID).First(&revision).Error; err != nil {
+	if err := repo.db.Preload("Resources").Preload("SourceConfigs").Preload("EnvGroups").Where("id = ?", stackRevisionID).First(&revision).Error; err != nil {
 		return nil, err
 	}
 
@@ -137,7 +139,7 @@ func (repo *StackRepository) ReadStackRevision(stackRevisionID uint) (*models.St
 func (repo *StackRepository) ReadStackRevisionByNumber(stackID uint, revisionNumber uint) (*models.StackRevision, error) {
 	revision := &models.StackRevision{}
 
-	if err := repo.db.Preload("Resources").Preload("SourceConfigs").Where("stack_id = ? AND revision_number = ?", stackID, revisionNumber).First(&revision).Error; err != nil {
+	if err := repo.db.Preload("Resources").Preload("SourceConfigs").Preload("EnvGroups").Where("stack_id = ? AND revision_number = ?", stackID, revisionNumber).First(&revision).Error; err != nil {
 		return nil, err
 	}
 
@@ -181,3 +183,13 @@ func (repo *StackRepository) UpdateStackResource(resource *models.StackResource)
 
 	return resource, nil
 }
+
+func (repo *StackRepository) ReadStackEnvGroupFirstMatch(projectID, clusterID uint, namespace, name string) (*models.StackEnvGroup, error) {
+	envGroup := &models.StackEnvGroup{}
+
+	if err := repo.db.Where("project_id = ? AND cluster_id = ? AND namespace = ? AND name = ?", projectID, clusterID, namespace, name).Order("id desc").First(&envGroup).Error; err != nil {
+		return nil, err
+	}
+
+	return envGroup, nil
+}

+ 2 - 0
internal/repository/stack.go

@@ -17,4 +17,6 @@ type StackRepository interface {
 
 	ReadStackResource(resourceID uint) (*models.StackResource, error)
 	UpdateStackResource(resource *models.StackResource) (*models.StackResource, error)
+
+	ReadStackEnvGroupFirstMatch(projectID, clusterID uint, namespace, name string) (*models.StackEnvGroup, error)
 }

+ 23 - 0
internal/stacks/helpers.go

@@ -76,3 +76,26 @@ func CloneAppResources(
 
 	return res, nil
 }
+
+func CloneEnvGroups(envGroups []models.StackEnvGroup) ([]models.StackEnvGroup, error) {
+	res := make([]models.StackEnvGroup, 0)
+
+	for _, envGroup := range envGroups {
+		uid, err := encryption.GenerateRandomBytes(16)
+
+		if err != nil {
+			return nil, err
+		}
+
+		res = append(res, models.StackEnvGroup{
+			UID:             uid,
+			Name:            envGroup.Name,
+			EnvGroupVersion: envGroup.EnvGroupVersion,
+			Namespace:       envGroup.Namespace,
+			ProjectID:       envGroup.ProjectID,
+			ClusterID:       envGroup.ClusterID,
+		})
+	}
+
+	return res, nil
+}

+ 79 - 0
internal/stacks/hooks.go

@@ -1,9 +1,11 @@
 package stacks
 
 import (
+	"errors"
 	"fmt"
 
 	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
 	"helm.sh/helm/v3/pkg/release"
 )
@@ -65,10 +67,87 @@ func UpdateHelmRevision(config *config.Config, projID, clusterID uint, rel *rele
 		}
 	}
 
+	clonedEnvGroups, err := CloneEnvGroups(stackRevision.EnvGroups)
+
+	if err != nil {
+		return err
+	}
+
+	stackRevision.Model = gorm.Model{}
+	stackRevision.RevisionNumber++
+	stackRevision.Resources = clonedAppResources
+	stackRevision.SourceConfigs = clonedSourceConfigs
+	stackRevision.EnvGroups = clonedEnvGroups
+	stackRevision.Status = "deployed"
+
+	_, err = config.Repo.Stack().AppendNewRevision(stackRevision)
+
+	return err
+}
+
+func UpdateEnvGroupVersion(config *config.Config, projID, clusterID uint, envGroup *types.EnvGroup) error {
+	// read stack env group by params
+	stackEnvGroup, err := config.Repo.Stack().ReadStackEnvGroupFirstMatch(projID, clusterID, envGroup.Namespace, envGroup.Name)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			return nil
+		}
+
+		return err
+	}
+
+	// read the revision number corresponding and create a new revision of the stack
+	oldStackRevision, err := config.Repo.Stack().ReadStackRevision(stackEnvGroup.StackRevisionID)
+
+	if err != nil {
+		return err
+	}
+
+	// get the latest revision for that stack
+	stack, err := config.Repo.Stack().ReadStackByID(projID, oldStackRevision.StackID)
+
+	if err != nil {
+		return err
+	}
+
+	if len(stack.Revisions) == 0 {
+		return fmt.Errorf("length of stack revision list was 0")
+	}
+
+	currStackRevision := stack.Revisions[0]
+	stackRevision := &currStackRevision
+
+	clonedSourceConfigs, err := CloneSourceConfigs(stackRevision.SourceConfigs)
+
+	if err != nil {
+		return err
+	}
+
+	clonedAppResources, err := CloneAppResources(stackRevision.Resources, stackRevision.SourceConfigs, clonedSourceConfigs)
+
+	if err != nil {
+		return err
+	}
+
+	clonedEnvGroups, err := CloneEnvGroups(stackRevision.EnvGroups)
+
+	if err != nil {
+		return err
+	}
+
+	for _, clonedEnvGroup := range clonedEnvGroups {
+		if clonedEnvGroup.Name == envGroup.Name {
+			clonedEnvGroup.EnvGroupVersion = envGroup.Version
+		}
+	}
+
 	stackRevision.Model = gorm.Model{}
 	stackRevision.RevisionNumber++
 	stackRevision.Resources = clonedAppResources
 	stackRevision.SourceConfigs = clonedSourceConfigs
+	stackRevision.EnvGroups = clonedEnvGroups
+	stackRevision.Status = "deployed"
 
 	_, err = config.Repo.Stack().AppendNewRevision(stackRevision)