Browse Source

finalize apply for preview env porter yaml

Ian Edwards 2 năm trước cách đây
mục cha
commit
0d83af2551

+ 29 - 0
api/client/environment_config.go

@@ -0,0 +1,29 @@
+package client
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/porter-dev/porter/api/types"
+)
+
+
+func (c *Client) GetEnvironmentConfig(
+	ctx context.Context,
+	projID, clusterID, envConfID uint,
+) (*types.EnvironmentConfig, error) {
+	resp := &types.EnvironmentConfig{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/env_config/%d",
+			projID,
+			clusterID,
+			envConfID,
+		),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}

+ 27 - 0
api/client/preview_environment.go

@@ -0,0 +1,27 @@
+package client
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/porter-dev/porter/api/types"
+)
+
+func (c *Client) CreatePreviewEnvironment(
+	ctx context.Context,
+	projectID, clusterID uint,
+	req *types.CreatePreviewEnvironmentRequest,
+) (*types.PreviewEnvironment, error) {
+	resp := &types.PreviewEnvironment{}
+
+	err := c.postRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/preview_environments",
+			projectID, clusterID,
+		),
+		req,
+		resp,
+	)
+
+	return resp, err
+}

+ 73 - 0
api/server/handlers/environment_config/get.go

@@ -0,0 +1,73 @@
+package environment_config
+
+import (
+	"errors"
+	"net/http"
+	"strconv"
+
+	"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/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+	"gorm.io/gorm"
+)
+
+type GetEnvConfigHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetEnvConfigHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetEnvConfigHandler {
+	return &GetEnvConfigHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *GetEnvConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-env-config")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	eci, reqErr := requestutils.GetURLParamString(r, types.URLParamEnvConfigID)
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, http.StatusBadRequest))
+		return
+	}
+
+	envConfigId, err := strconv.Atoi(eci)
+	if err != nil {
+		err = telemetry.Error(ctx, span, err, "Invalid configuration ID")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
+		telemetry.AttributeKV{Key: "project-id", Value: project.ID},
+		telemetry.AttributeKV{Key: "env-config-id", Value: envConfigId},
+	)
+
+	envConf, err := c.Repo().EnvironmentConfig().ReadEnvironmentConfig(project.ID, cluster.ID, uint(envConfigId))
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			err = telemetry.Error(ctx, span, err, "Environment config not found")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
+			return
+		}
+		err = telemetry.Error(ctx, span, err, "Failed to read environment config")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, envConf.ToEnvironmentConfigType())
+}

+ 6 - 1
api/server/handlers/porter_app/create.go

@@ -64,8 +64,13 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
-	namespace := fmt.Sprintf("porter-stack-%s", stackName)
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "application-name", Value: stackName})
+	var namespace string
+	if request.Namespace != "" {
+		namespace = request.Namespace
+	} else {
+		namespace = fmt.Sprintf("porter-stack-%s", stackName)
+	}
 
 	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, namespace)
 	if err != nil {

+ 69 - 0
api/server/handlers/preview_environment/create.go

@@ -0,0 +1,69 @@
+package preview_environment
+
+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/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+type CreatePreviewEnvironmentHandler struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewCreatePreviewEnvironmentHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreatePreviewEnvironmentHandler {
+	return &CreatePreviewEnvironmentHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (c *CreatePreviewEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-preview-env")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
+
+	request := &types.CreatePreviewEnvironmentRequest{}
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		err := telemetry.Error(ctx, span, nil, "error decoding request")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
+		telemetry.AttributeKV{Key: "project-id", Value: project.ID},
+		telemetry.AttributeKV{Key: "env-config-id", Value: request.EnvironmentConfigID},
+		telemetry.AttributeKV{Key: "git-repo-owner", Value: request.GitRepoOwner},
+		telemetry.AttributeKV{Key: "git-repo-name", Value: request.GitRepoName},
+		telemetry.AttributeKV{Key: "branch", Value: request.Branch},
+	)
+
+	// create the preview environment
+	previewEnv := &models.PreviewEnvironment{
+		GitRepoOwner:        request.GitRepoOwner,
+		GitRepoName:         request.GitRepoName,
+		Branch:              request.Branch,
+		EnvironmentConfigID: request.EnvironmentConfigID,
+	}
+
+	previewEnv, err := c.Repo().PreviewEnvironment().CreatePreviewEnvironment(previewEnv)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error creating preview environment")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, previewEnv.ToPreviewEnvironmentType())
+	return
+}

+ 87 - 0
api/server/router/environment_config.go

@@ -0,0 +1,87 @@
+package router
+
+import (
+	"fmt"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/porter-dev/porter/api/server/handlers/environment_config"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewEnvConfigScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetEnvConfigScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetEnvConfigScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getEnvConfigRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getEnvConfigRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/env_config"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	var routes []*router.Route
+
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/env_config/{env_config_id} -> env_config.NewGetEnvConfigHandler
+	getEnvConfigEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamEnvConfigID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getEnvConfigHandler := environment_config.NewGetEnvConfigHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getEnvConfigEndpoint,
+		Handler:  getEnvConfigHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 88 - 0
api/server/router/preview_environment.go

@@ -0,0 +1,88 @@
+package router
+
+import (
+	"fmt"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/porter-dev/porter/api/server/handlers/preview_environment"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/router"
+	"github.com/porter-dev/porter/api/types"
+)
+
+func NewPreviewEnvironmentScopedRegisterer(children ...*router.Registerer) *router.Registerer {
+	return &router.Registerer{
+		GetRoutes: GetPreviewEnvironmentScopedRoutes,
+		Children:  children,
+	}
+}
+
+func GetPreviewEnvironmentScopedRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+	children ...*router.Registerer,
+) []*router.Route {
+	routes, projPath := getPreviewEnvRoutes(r, config, basePath, factory)
+
+	if len(children) > 0 {
+		r.Route(projPath.RelativePath, func(r chi.Router) {
+			for _, child := range children {
+				childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...)
+
+				routes = append(routes, childRoutes...)
+			}
+		})
+	}
+
+	return routes
+}
+
+func getPreviewEnvRoutes(
+	r chi.Router,
+	config *config.Config,
+	basePath *types.Path,
+	factory shared.APIEndpointFactory,
+) ([]*router.Route, *types.Path) {
+	relPath := "/preview_environment"
+
+	newPath := &types.Path{
+		Parent:       basePath,
+		RelativePath: relPath,
+	}
+
+	var routes []*router.Route
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/preview_environment -> preview_environment.NewCreatePreviewEnvironmentHandler
+	createPreviewEnvEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}", relPath, types.URLParamStackName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	createPreviewEnvHandler := preview_environment.NewCreatePreviewEnvironmentHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: createPreviewEnvEndpoint,
+		Handler:  createPreviewEnvHandler,
+		Router:   r,
+	})
+
+	return routes, newPath
+}

+ 2 - 1
api/server/router/router.go

@@ -32,7 +32,8 @@ func NewAPIRouter(config *config.Config) *chi.Mux {
 	namespaceRegisterer := NewNamespaceScopedRegisterer(releaseRegisterer)
 	clusterIntegrationRegisterer := NewClusterIntegrationScopedRegisterer()
 	stackRegisterer := NewStackScopedRegisterer()
-	clusterRegisterer := NewClusterScopedRegisterer(namespaceRegisterer, clusterIntegrationRegisterer, stackRegisterer)
+	envConfigRegisterer := NewEnvConfigScopedRegisterer()
+	clusterRegisterer := NewClusterScopedRegisterer(namespaceRegisterer, clusterIntegrationRegisterer, stackRegisterer, envConfigRegisterer)
 	infraRegisterer := NewInfraScopedRegisterer()
 	gitInstallationRegisterer := NewGitInstallationScopedRegisterer()
 	registryRegisterer := NewRegistryScopedRegisterer()

+ 1 - 0
api/types/porter_app.go

@@ -50,6 +50,7 @@ type CreatePorterAppRequest struct {
 	EnvGroups        []string  `json:"env_groups"`
 	UserUpdate       bool      `json:"user_update"`
 	FullHelmValues   string    `json:"full_helm_values"`
+	Namespace        string    `json:"namespace"`
 }
 
 type UpdatePorterAppRequest struct {

+ 29 - 0
api/types/preview_environment.go

@@ -0,0 +1,29 @@
+package types
+
+type EnvironmentConfig struct {
+	ID                uint `json:"id"`
+	ProjectID         uint `json:"project_id"`
+	ClusterID         uint `json:"cluster_id"`
+	GitInstallationID uint `json:"git_installation_id"`
+
+	Name string `json:"string"`
+	Auto bool   `json:"auto"`
+}
+
+type CreatePreviewEnvironmentRequest struct {
+	EnvironmentConfigID uint   `json:"environment_config_id"`
+	GitRepoOwner        string `json:"git_repo_owner"`
+	GitRepoName         string `json:"git_repo_name"`
+	Branch              string `json:"branch"`
+}
+
+type PreviewEnvironment struct {
+	ID           uint   `json:"id"`
+	GitRepoOwner string `json:"git_repo_owner"`
+	GitRepoName  string `json:"git_repo_name"`
+	Branch       string `json:"branch"`
+
+	NewCommentsDisabled bool `json:"new_comments_disabled"`
+
+	EnvironmentConfigID uint `json:"environment_config_id"`
+}

+ 1 - 0
api/types/request.go

@@ -52,6 +52,7 @@ const (
 	URLParamStackName             URLParam = "stack_name"
 	URLParamStackEventID          URLParam = "stack_event_id"
 	URLParamPorterAppEventID      URLParam = "porter_app_event_id"
+	URLParamEnvConfigID           URLParam = "env_config_id"
 )
 
 type Path struct {

+ 21 - 15
cli/cmd/stack/apply.go

@@ -26,7 +26,18 @@ type StackConf struct {
 func CreateApplicationDeploy(client *api.Client, worker *switchboardWorker.Worker, app *Application, applicationName string, cliConf *config.CLIConfig) ([]*switchboardTypes.Resource, error) {
 	// we need to know the builder so that we can inject launcher to the start command later if heroku builder is used
 	var builder string
-	resources, builder, err := createV1BuildResources(client, app, applicationName, cliConf.Project, cliConf.Cluster)
+	
+	namespace, envMeta, err := HandleEnvironmentConfiguration(client, cliConf, applicationName)
+	if err != nil {
+		return nil, err
+	}
+	
+	stackConf, err := createStackConf(client, app, namespace, applicationName, cliConf.Project, cliConf.Cluster)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing porter.yaml: %w", err)
+	}
+
+	resources, builder, err := createV1BuildResources(client, app, stackConf)
 	if err != nil {
 		return nil, fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
 	}
@@ -44,6 +55,8 @@ func CreateApplicationDeploy(client *api.Client, worker *switchboardWorker.Worke
 		BuildImageDriverName: GetBuildImageDriverName(applicationName),
 		PorterYAML:           applicationBytes,
 		Builder:              builder,
+		Namespace:            namespace,
+		EnvironmentMeta:      envMeta,
 	}
 
 	worker.RegisterHook("deploy-stack", deployStackHook)
@@ -102,17 +115,9 @@ func createAppEvent(client *api.Client, applicationName string, cliConf *config.
 	return nil
 }
 
-func createV1BuildResources(client *api.Client, app *Application, stackName string, projectID uint, clusterID uint) ([]*switchboardTypes.Resource, string, error) {
-	var builder string
+func createV1BuildResources(client *api.Client, app *Application, stackConf *StackConf) ([]*switchboardTypes.Resource, string, error) {
 	resources := make([]*switchboardTypes.Resource, 0)
 
-	stackConf, err := createStackConf(client, app, stackName, projectID, clusterID)
-	if err != nil {
-		return nil, "", err
-	}
-
-	var bi, pi *switchboardTypes.Resource
-
 	// look up build settings from DB if none specified in porter.yaml
 	if stackConf.parsed.Build == nil {
 		color.New(color.FgYellow).Printf("No build values specified in porter.yaml, attempting to load stack build settings instead \n")
@@ -129,7 +134,7 @@ func createV1BuildResources(client *api.Client, app *Application, stackName stri
 
 	// only include build and push steps if an image is not already specified
 	if stackConf.parsed.Build.Image == nil {
-		bi, pi, builder, err = createV1BuildResourcesFromPorterYaml(stackConf)
+		bi, pi, builder, err := createV1BuildResourcesFromPorterYaml(stackConf)
 
 		if err != nil {
 			return nil, "", err
@@ -158,13 +163,14 @@ func createV1BuildResources(client *api.Client, app *Application, stackName stri
 		} else {
 			color.New(color.FgYellow).Printf("No pre-deploy command found in porter.yaml or helm. \n")
 		}
+
+		return resources, builder, nil
 	}
 
-	return resources, builder, nil
+	return resources, "", nil
 }
 
-func createStackConf(client *api.Client, app *Application, stackName string, projectID uint, clusterID uint) (*StackConf, error) {
-
+func createStackConf(client *api.Client, app *Application, namespace string, stackName string, projectID uint, clusterID uint) (*StackConf, error) {
 	err := config.ValidateCLIEnvironment()
 	if err != nil {
 		errMsg := composePreviewMessage("porter CLI is not configured correctly", Error)
@@ -183,7 +189,7 @@ func createStackConf(client *api.Client, app *Application, stackName string, pro
 		stackName: stackName,
 		projectID: projectID,
 		clusterID: clusterID,
-		namespace: fmt.Sprintf("porter-stack-%s", stackName),
+		namespace: namespace,
 	}, nil
 }
 

+ 94 - 0
cli/cmd/stack/environment.go

@@ -0,0 +1,94 @@
+package stack
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/cli/cmd/config"
+)
+
+type GitHubMetadata struct {
+	Repo, RepoOwner, BranchFrom, PRName string
+}
+
+type EnvironmentMeta struct {
+	EnvironmentConfigID uint            `json:"environment_config_id"`
+	Namespace           string          `json:"namespace"`
+	GitHubMetadata      *GitHubMetadata `json:"github_metadata"`
+}
+
+func HandleEnvironmentConfiguration(
+	client *api.Client,
+	cliConf *config.CLIConfig,
+	applicationName string,
+) (string, *EnvironmentMeta, error) {
+	var namespace string
+	var envMeta *EnvironmentMeta
+
+	environmentConfigID := os.Getenv("PORTER_ENVIRONMENT_ID")
+	if environmentConfigID != "" {
+		eci, err := strconv.Atoi(environmentConfigID)
+		if err != nil {
+			return "", nil, fmt.Errorf("unable to parse PORTER_ENVIRONMENT_ID: %w", err)
+		}
+
+		ghMeta, err := getGitDeployMeta()
+		if err != nil {
+			return "", nil, fmt.Errorf("unable to deploy to environmet: %w", err)
+		}
+
+		envConf, err := client.GetEnvironmentConfig(context.Background(), cliConf.Project, cliConf.Cluster, uint(eci))
+
+		if err != nil {
+			return "", nil, fmt.Errorf("unable to read environment config from DB: %w", err)
+		}
+
+		namespace = formatNamespaceForEnvironment(envConf.Name, ghMeta.RepoOwner, ghMeta.Repo, ghMeta.BranchFrom)
+
+		envMeta = &EnvironmentMeta{
+			EnvironmentConfigID: uint(eci),
+			Namespace:           namespace,
+			GitHubMetadata:      ghMeta,
+		}
+	} else {
+		namespace = fmt.Sprintf("porter-stack-%s", applicationName)
+	}
+
+	return namespace, envMeta, nil
+}
+
+func formatNamespaceForEnvironment(envName, repoOwner, repo, branch string) string {
+	return fmt.Sprintf("porter-env-%s-%s-%s-%s", envName, repoOwner, repo, branch)
+}
+
+func getGitDeployMeta() (*GitHubMetadata, error) {
+	branchFrom := os.Getenv("PORTER_BRANCH_FROM")
+	if branchFrom == "" {
+		return nil, fmt.Errorf("PORTER_BRANCH_FROM not set")
+	}
+
+	repoName := os.Getenv("PORTER_REPO_NAME")
+	if repoName == "" {
+		return nil, fmt.Errorf("PORTER_REPO_NAME not set")
+	}
+
+	repoOwner := os.Getenv("PORTER_REPO_OWNER")
+	if repoOwner == "" {
+		return nil, fmt.Errorf("PORTER_REPO_OWNER not set")
+	}
+
+	prName := os.Getenv("PORTER_PR_NAME")
+	if prName == "" {
+		return nil, fmt.Errorf("PORTER_PR_NAME not set")
+	}
+
+	return &GitHubMetadata{
+		RepoOwner:  repoOwner,
+		Repo:       repoName,
+		BranchFrom: branchFrom,
+		PRName:     prName,
+	}, nil
+}

+ 20 - 10
cli/cmd/stack/hooks.go

@@ -19,6 +19,8 @@ type DeployAppHook struct {
 	BuildImageDriverName string
 	PorterYAML           []byte
 	Builder              string
+	Namespace            string
+	EnvironmentMeta      *EnvironmentMeta
 }
 
 func (t *DeployAppHook) PreApply() error {
@@ -40,13 +42,12 @@ func (t *DeployAppHook) DataQueries() map[string]interface{} {
 // deploy the app
 func (t *DeployAppHook) PostApply(driverOutput map[string]interface{}) error {
 	client := config.GetAPIClient()
-	namespace := fmt.Sprintf("porter-stack-%s", t.ApplicationName)
 
 	_, err := client.GetRelease(
 		context.Background(),
 		t.ProjectID,
 		t.ClusterID,
-		namespace,
+		t.Namespace,
 		t.ApplicationName,
 	)
 
@@ -77,20 +78,24 @@ func (t *DeployAppHook) applyApp(client *api.Client, shouldCreate bool, driverOu
 		}
 	}
 
+	req := &types.CreatePorterAppRequest{
+		ClusterID:        t.ClusterID,
+		ProjectID:        t.ProjectID,
+		PorterYAMLBase64: base64.StdEncoding.EncodeToString(t.PorterYAML),
+		ImageInfo:        imageInfo,
+		OverrideRelease:  false, // deploying from the cli will never delete release resources, only append or override
+		Builder:          t.Builder,
+		Namespace:        t.Namespace,
+	}
+
 	_, err := client.CreatePorterApp(
 		context.Background(),
 		t.ProjectID,
 		t.ClusterID,
 		t.ApplicationName,
-		&types.CreatePorterAppRequest{
-			ClusterID:        t.ClusterID,
-			ProjectID:        t.ProjectID,
-			PorterYAMLBase64: base64.StdEncoding.EncodeToString(t.PorterYAML),
-			ImageInfo:        imageInfo,
-			OverrideRelease:  false, // deploying from the cli will never delete release resources, only append or override
-			Builder:          t.Builder,
-		},
+		req,
 	)
+
 	if err != nil {
 		if shouldCreate {
 			return fmt.Errorf("error creating app %s: %w", t.ApplicationName, err)
@@ -98,6 +103,11 @@ func (t *DeployAppHook) applyApp(client *api.Client, shouldCreate bool, driverOu
 		return fmt.Errorf("error updating app %s: %w", t.ApplicationName, err)
 	}
 
+	if t.EnvironmentMeta != nil {
+		// create preview env record
+
+	}
+
 	return nil
 }
 

+ 35 - 0
internal/models/environment_config.go

@@ -0,0 +1,35 @@
+package models
+
+import (
+	"github.com/porter-dev/porter/api/types"
+	"gorm.io/gorm"
+)
+
+// Environment Config type used to set up an instance of an environment
+
+type EnvironmentConfig struct {
+	gorm.Model
+
+	ProjectID         uint
+	ClusterID         uint
+	GitInstallationID uint
+
+	WebhookID       string `gorm:"unique"`
+	GithubWebhookID int64
+
+	Name string
+	Auto bool
+
+	PreviewEnvironments []PreviewEnvironment
+}
+
+func (c *EnvironmentConfig) ToEnvironmentConfigType() *types.EnvironmentConfig {
+	return &types.EnvironmentConfig{
+		ID:                c.Model.ID,
+		ProjectID:         c.ProjectID,
+		ClusterID:         c.ClusterID,
+		GitInstallationID: c.GitInstallationID,
+		Name:              c.Name,
+		Auto:              c.Auto,
+	}
+}

+ 30 - 0
internal/models/preview_environment.go

@@ -0,0 +1,30 @@
+package models
+
+import (
+	"github.com/porter-dev/porter/api/types"
+	"gorm.io/gorm"
+)
+
+type PreviewEnvironment struct {
+	gorm.Model
+
+	GitRepoOwner string
+	GitRepoName  string
+	Branch       string
+
+	NewCommentsDisabled bool
+
+	EnvironmentConfigID uint
+	EnvironmentConfig   EnvironmentConfig
+}
+
+func (p *PreviewEnvironment) ToPreviewEnvironmentType() *types.PreviewEnvironment {
+	return &types.PreviewEnvironment{
+		ID:                  p.Model.ID,
+		GitRepoOwner:        p.GitRepoOwner,
+		GitRepoName:         p.GitRepoName,
+		Branch:              p.Branch,
+		NewCommentsDisabled: p.NewCommentsDisabled,
+		EnvironmentConfigID: p.EnvironmentConfigID,
+	}
+}

+ 11 - 0
internal/repository/environment_config.go

@@ -0,0 +1,11 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+)
+
+
+// EnvironmentConfigRepository represents the set of queries on the EnvironmentConfig model
+type EnvironmentConfigRepository interface {
+	ReadEnvironmentConfig(projectID, clusterID, id uint) (*models.EnvironmentConfig, error)
+}

+ 32 - 0
internal/repository/gorm/environment_config.go

@@ -0,0 +1,32 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// EnvironmentConfigRepository uses gorm.DB for querying the database
+type EnvironmentConfigRepository struct {
+	db *gorm.DB
+}
+
+// NewEnvironmentConfigRepository returns a EnvironmentConfigRepository which uses
+// gorm.DB for querying the database
+func NewEnvironmentConfigRepository(db *gorm.DB) repository.EnvironmentConfigRepository {
+	return &EnvironmentConfigRepository{db}
+}
+
+// ReadEnvironmentConfig gets an env config specified by a unique id
+func (repo *EnvironmentConfigRepository) ReadEnvironmentConfig(projectID, clusterID, id uint) (*models.EnvironmentConfig, error) {
+	env_config := &models.EnvironmentConfig{}
+
+	if err := repo.db.Order("id desc").Where(
+		"project_id = ? AND cluster_id = ? AND id = ?",
+		projectID, clusterID, id,
+	).First(&env_config).Error; err != nil {
+		return nil, err
+	}
+
+	return env_config, nil
+}

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

@@ -62,6 +62,8 @@ func AutoMigrate(db *gorm.DB, debug bool) error {
 		&models.AWSAssumeRoleChain{},
 		&models.PorterApp{},
 		&models.PorterAppEvent{},
+		&models.EnvironmentConfig{},
+		&models.PreviewEnvironment{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},

+ 25 - 0
internal/repository/gorm/preview_environment.go

@@ -0,0 +1,25 @@
+package gorm
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// PreviewEnvironmentRepository uses gorm.DB for querying the database
+type PreviewEnvironmentRepository struct {
+	db *gorm.DB
+}
+
+// NewPreviewEnvironmentRepository returns a PreviewEnvironmentRepository which uses
+// gorm.DB for querying the database
+func NewPreviewEnvironmentRepository(db *gorm.DB) repository.PreviewEnvironmentRepository {
+	return &PreviewEnvironmentRepository{db}
+}
+
+func (repo *PreviewEnvironmentRepository) CreatePreviewEnvironment(a *models.PreviewEnvironment) (*models.PreviewEnvironment, error) {
+	if err := repo.db.Create(a).Error; err != nil {
+		return nil, err
+	}
+	return a, nil
+}

+ 12 - 0
internal/repository/gorm/repository.go

@@ -53,6 +53,8 @@ type GormRepository struct {
 	awsAssumeRoleChainer      repository.AWSAssumeRoleChainer
 	porterApp                 repository.PorterAppRepository
 	porterAppEvent            repository.PorterAppEventRepository
+	environmentConfig         repository.EnvironmentConfigRepository
+	previewEnvironment        repository.PreviewEnvironmentRepository
 }
 
 func (t *GormRepository) User() repository.UserRepository {
@@ -239,6 +241,14 @@ func (t *GormRepository) PorterAppEvent() repository.PorterAppEventRepository {
 	return t.porterAppEvent
 }
 
+func (t *GormRepository) EnvironmentConfig() repository.EnvironmentConfigRepository {
+	return t.environmentConfig
+}
+
+func (t *GormRepository) PreviewEnvironment() repository.PreviewEnvironmentRepository {
+	return t.previewEnvironment
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.CredentialStorage) repository.Repository {
@@ -289,5 +299,7 @@ func NewRepository(db *gorm.DB, key *[32]byte, storageBackend credentials.Creden
 		awsAssumeRoleChainer:      NewAWSAssumeRoleChainer(db),
 		porterApp:                 NewPorterAppRepository(db),
 		porterAppEvent:            NewPorterAppEventRepository(db),
+		environmentConfig:         NewEnvironmentConfigRepository(db),
+		previewEnvironment:        NewPreviewEnvironmentRepository(db),
 	}
 }

+ 10 - 0
internal/repository/preview_environment.go

@@ -0,0 +1,10 @@
+package repository
+
+import (
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// PreviewEnvironmentRepository represents the set of queries on the PreviewEnvironment model
+type PreviewEnvironmentRepository interface {
+	CreatePreviewEnvironment(a *models.PreviewEnvironment) (*models.PreviewEnvironment, error)
+}

+ 2 - 0
internal/repository/repository.go

@@ -47,4 +47,6 @@ type Repository interface {
 	AWSAssumeRoleChainer() AWSAssumeRoleChainer
 	PorterApp() PorterAppRepository
 	PorterAppEvent() PorterAppEventRepository
+	EnvironmentConfig() EnvironmentConfigRepository
+	PreviewEnvironment() PreviewEnvironmentRepository
 }

+ 22 - 0
internal/repository/test/environment_config.go

@@ -0,0 +1,22 @@
+package test
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type EnvironmentConfigRepository struct {
+	canQuery       bool
+	failingMethods string
+}
+
+func NewEnvironmentConfigRepository(canQuery bool, failingMethods ...string) repository.EnvironmentConfigRepository {
+	return &EnvironmentConfigRepository{canQuery, strings.Join(failingMethods, ",")}
+}
+
+func (repo *EnvironmentConfigRepository) ReadEnvironmentConfig(projectID, clusterID, id uint) (*models.EnvironmentConfig, error) {
+	return nil, errors.New("cannot write database")
+}

+ 22 - 0
internal/repository/test/preview_environment.go

@@ -0,0 +1,22 @@
+package test
+
+import (
+	"errors"
+	"strings"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+)
+
+type PreviewEnvironmentRepository struct {
+	canQuery       bool
+	failingMethods string
+}
+
+func NewPreviewEnvironmentRepository(canQuery bool, failingMethods ...string) repository.PreviewEnvironmentRepository {
+	return &PreviewEnvironmentRepository{canQuery, strings.Join(failingMethods, ",")}
+}
+
+func (repo *PreviewEnvironmentRepository) CreatePreviewEnvironment(a *models.PreviewEnvironment) (*models.PreviewEnvironment, error) {
+	return nil, errors.New("cannot write database")
+}

+ 12 - 0
internal/repository/test/repository.go

@@ -51,6 +51,8 @@ type TestRepository struct {
 	awsAssumeRoleChainer      repository.AWSAssumeRoleChainer
 	porterApp                 repository.PorterAppRepository
 	porterAppEvent            repository.PorterAppEventRepository
+	environmentConfig         repository.EnvironmentConfigRepository
+	previewEnvironment        repository.PreviewEnvironmentRepository
 }
 
 func (t *TestRepository) User() repository.UserRepository {
@@ -237,6 +239,14 @@ func (t *TestRepository) PorterAppEvent() repository.PorterAppEventRepository {
 	return t.porterAppEvent
 }
 
+func (t *TestRepository) EnvironmentConfig() repository.EnvironmentConfigRepository {
+	return t.environmentConfig
+}
+
+func (t *TestRepository) PreviewEnvironment() repository.PreviewEnvironmentRepository {
+	return t.previewEnvironment
+}
+
 // NewRepository returns a Repository which persists users in memory
 // and accepts a parameter that can trigger read/write errors
 func NewRepository(canQuery bool, failingMethods ...string) repository.Repository {
@@ -287,5 +297,7 @@ func NewRepository(canQuery bool, failingMethods ...string) repository.Repositor
 		awsAssumeRoleChainer:      NewAWSAssumeRoleChainer(),
 		porterApp:                 NewPorterAppRepository(canQuery, failingMethods...),
 		porterAppEvent:            NewPorterAppEventRepository(canQuery),
+		environmentConfig:         NewEnvironmentConfigRepository(canQuery),
+		previewEnvironment:        NewPreviewEnvironmentRepository(canQuery),
 	}
 }