Quellcode durchsuchen

associate stack with env

Ian Edwards vor 2 Jahren
Ursprung
Commit
3e5b7502c8

+ 22 - 1
api/client/environment_config.go

@@ -7,7 +7,6 @@ import (
 	"github.com/porter-dev/porter/api/types"
 )
 
-
 func (c *Client) GetEnvironmentConfig(
 	ctx context.Context,
 	projID, clusterID, envConfID uint,
@@ -27,3 +26,25 @@ func (c *Client) GetEnvironmentConfig(
 
 	return resp, err
 }
+
+func (c *Client) GetPorterAppByEnvironment(
+	ctx context.Context,
+	projID, clusterID, envConfID uint,
+	stackName string,
+) (*types.PorterApp, error) {
+	resp := &types.PorterApp{}
+
+	err := c.getRequest(
+		fmt.Sprintf(
+			"/projects/%d/clusters/%d/env_config/%d/stacks/%s",
+			projID,
+			clusterID,
+			envConfID,
+			stackName,
+		),
+		nil,
+		resp,
+	)
+
+	return resp, err
+}

+ 80 - 0
api/server/handlers/environment_config/get_stack.go

@@ -0,0 +1,80 @@
+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 GetEnvConfigStackHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetEnvConfigStackHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *GetEnvConfigStackHandler {
+	return &GetEnvConfigStackHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (c *GetEnvConfigStackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-env-config-stack")
+	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
+	}
+
+	stackName, reqErr := requestutils.GetURLParamString(r, types.URLParamStackName)
+	if reqErr != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(reqErr, 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},
+		telemetry.AttributeKV{Key: "stack-name", Value: stackName},
+	)
+
+	app, err := c.Repo().PorterApp().ReadPorterAppByNameInEnvironment(cluster.ID, stackName, uint(envConfigId))
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			err = telemetry.Error(ctx, span, err, "App not found for environment config")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
+			return
+		}
+		err = telemetry.Error(ctx, span, err, "Failed to read app from DB")
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	c.WriteResult(w, r, app.ToPorterAppType())
+}

+ 46 - 16
api/server/handlers/porter_app/create.go

@@ -12,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
 	"github.com/porter-dev/porter/internal/telemetry"
+	"gorm.io/gorm"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -240,17 +241,6 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			return
 		}
 
-		existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
-		if err != nil {
-			err = telemetry.Error(ctx, span, err, "error reading app from DB")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-			return
-		} else if existing.Name != "" {
-			err = telemetry.Error(ctx, span, err, "app with name already exists in project")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
-			return
-		}
-
 		app := &models.PorterApp{
 			Name:      stackName,
 			ClusterID: cluster.ID,
@@ -268,6 +258,35 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			PorterYamlPath: request.PorterYamlPath,
 		}
 
+		if request.EnvironmentConfigID != 0 {
+			existing, err := c.Repo().PorterApp().ReadPorterAppByNameInEnvironment(cluster.ID, stackName, request.EnvironmentConfigID)
+			if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+				err = telemetry.Error(ctx, span, err, "error reading app from DB")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			}
+
+			if existing != nil {
+				err = telemetry.Error(ctx, span, err, "app with name already exists in environment")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
+				return
+			}
+
+			app.EnvironmentConfigID = request.EnvironmentConfigID
+
+		} else {
+			existing, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+			if err != nil {
+				err = telemetry.Error(ctx, span, err, "error reading app from DB")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			} else if existing.Name != "" {
+				err = telemetry.Error(ctx, span, err, "app with name already exists in project")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
+				return
+			}
+		}
+
 		// create the db entry
 		porterApp, err := c.Repo().PorterApp().UpdatePorterApp(app)
 		if err != nil {
@@ -380,12 +399,23 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		}
 
 		// update the DB entry
-		app, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
-		if err != nil {
-			err = telemetry.Error(ctx, span, err, "error reading app from DB")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
-			return
+		var app *models.PorterApp
+		if request.EnvironmentConfigID != 0 {
+			app, err = c.Repo().PorterApp().ReadPorterAppByNameInEnvironment(cluster.ID, stackName, request.EnvironmentConfigID)
+			if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
+				err = telemetry.Error(ctx, span, err, "error reading app from DB")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			}
+		} else {
+			app, err = c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, stackName)
+			if err != nil {
+				err = telemetry.Error(ctx, span, err, "error reading app from DB")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+				return
+			}
 		}
+
 		if app == nil {
 			err = telemetry.Error(ctx, span, nil, "app with name does not exist in project")
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))

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

@@ -55,6 +55,34 @@ func getEnvConfigRoutes(
 
 	var routes []*router.Route
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/env_config/{env_config_id}/stacks/{name}
+	getEnvConfigStackEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("%s/{%s}/stacks/{%s}", relPath, types.URLParamEnvConfigID, types.URLParamStackName),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getEnvConfigStackHandler := environment_config.NewGetEnvConfigStackHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getEnvConfigStackEndpoint,
+		Handler:  getEnvConfigStackHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/clusters/{cluster_id}/env_config/{env_config_id} -> env_config.NewGetEnvConfigHandler
 	getEnvConfigEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 3 - 5
api/server/router/preview_environment.go

@@ -1,8 +1,6 @@
 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"
@@ -46,7 +44,7 @@ func getPreviewEnvRoutes(
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*router.Route, *types.Path) {
-	relPath := "/preview_environment"
+	relPath := "/preview_environments"
 
 	newPath := &types.Path{
 		Parent:       basePath,
@@ -55,14 +53,14 @@ func getPreviewEnvRoutes(
 
 	var routes []*router.Route
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/preview_environment -> preview_environment.NewCreatePreviewEnvironmentHandler
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/preview_environments -> 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),
+				RelativePath: relPath,
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,

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

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

+ 20 - 19
api/types/porter_app.go

@@ -32,25 +32,26 @@ type PorterApp struct {
 
 // swagger:model
 type CreatePorterAppRequest struct {
-	ClusterID        uint      `json:"cluster_id"`
-	ProjectID        uint      `json:"project_id"`
-	RepoName         string    `json:"repo_name"`
-	GitBranch        string    `json:"git_branch"`
-	GitRepoID        uint      `json:"git_repo_id"`
-	BuildContext     string    `json:"build_context"`
-	Builder          string    `json:"builder"`
-	Buildpacks       string    `json:"buildpacks"`
-	Dockerfile       string    `json:"dockerfile"`
-	ImageRepoURI     string    `json:"image_repo_uri"`
-	PullRequestURL   string    `json:"pull_request_url"`
-	PorterYAMLBase64 string    `json:"porter_yaml"`
-	PorterYamlPath   string    `json:"porter_yaml_path"`
-	ImageInfo        ImageInfo `json:"image_info" form:"omitempty"`
-	OverrideRelease  bool      `json:"override_release"`
-	EnvGroups        []string  `json:"env_groups"`
-	UserUpdate       bool      `json:"user_update"`
-	FullHelmValues   string    `json:"full_helm_values"`
-	Namespace        string    `json:"namespace"`
+	ClusterID           uint      `json:"cluster_id"`
+	ProjectID           uint      `json:"project_id"`
+	RepoName            string    `json:"repo_name"`
+	GitBranch           string    `json:"git_branch"`
+	GitRepoID           uint      `json:"git_repo_id"`
+	BuildContext        string    `json:"build_context"`
+	Builder             string    `json:"builder"`
+	Buildpacks          string    `json:"buildpacks"`
+	Dockerfile          string    `json:"dockerfile"`
+	ImageRepoURI        string    `json:"image_repo_uri"`
+	PullRequestURL      string    `json:"pull_request_url"`
+	PorterYAMLBase64    string    `json:"porter_yaml"`
+	PorterYamlPath      string    `json:"porter_yaml_path"`
+	ImageInfo           ImageInfo `json:"image_info" form:"omitempty"`
+	OverrideRelease     bool      `json:"override_release"`
+	EnvGroups           []string  `json:"env_groups"`
+	UserUpdate          bool      `json:"user_update"`
+	FullHelmValues      string    `json:"full_helm_values"`
+	Namespace           string    `json:"namespace"`
+	EnvironmentConfigID uint      `json:"environment_config_id"`
 }
 
 type UpdatePorterAppRequest struct {

+ 19 - 9
cli/cmd/stack/apply.go

@@ -26,20 +26,20 @@ 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
-	
+
 	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)
+	resources, builder, err := createV1BuildResources(client, app, stackConf, envMeta)
 	if err != nil {
-		return nil, fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
+		return nil, err
 	}
 
 	applicationBytes, err := yaml.Marshal(app)
@@ -115,20 +115,30 @@ func createAppEvent(client *api.Client, applicationName string, cliConf *config.
 	return nil
 }
 
-func createV1BuildResources(client *api.Client, app *Application, stackConf *StackConf) ([]*switchboardTypes.Resource, string, error) {
+func createV1BuildResources(client *api.Client, app *Application, stackConf *StackConf, envMeta EnvironmentMeta) ([]*switchboardTypes.Resource, string, error) {
 	resources := make([]*switchboardTypes.Resource, 0)
 
 	// 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")
 
-		res, err := client.GetPorterApp(context.Background(), stackConf.projectID, stackConf.clusterID, stackConf.stackName)
+		var converted Build
+		if envMeta.EnvironmentConfigID == 0 {
+			res, err := client.GetPorterApp(context.Background(), stackConf.projectID, stackConf.clusterID, stackConf.stackName)
+			if err != nil {
+				return nil, "", fmt.Errorf("unable to read build info from DB: %w", err)
+			}
+			converted = convertToBuild(res)
+		} else {
+			color.New(color.FgYellow).Printf("Looking for application %s in specified environment \n", stackConf.stackName)
 
-		if err != nil {
-			return nil, "", fmt.Errorf("unable to read build info from DB: %w", err)
+			res, err := client.GetPorterAppByEnvironment(context.Background(), stackConf.projectID, stackConf.clusterID, envMeta.EnvironmentConfigID, stackConf.stackName)
+			if err != nil {
+				return nil, "", fmt.Errorf("unable to read build info from DB: %w", err)
+			}
+			converted = convertToBuild(res)
 		}
 
-		converted := convertToBuild(res)
 		stackConf.parsed.Build = &converted
 	}
 

+ 23 - 24
cli/cmd/stack/environment.go

@@ -15,44 +15,42 @@ type GitHubMetadata struct {
 }
 
 type EnvironmentMeta struct {
-	EnvironmentConfigID uint            `json:"environment_config_id"`
-	Namespace           string          `json:"namespace"`
-	GitHubMetadata      *GitHubMetadata `json:"github_metadata"`
+	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) {
+) (string, EnvironmentMeta, error) {
 	var namespace string
-	var envMeta *EnvironmentMeta
+	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)
+			return "", envMeta, 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)
+			return "", envMeta, 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)
+			return "", envMeta, 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,
-		}
+		envMeta.EnvironmentConfigID = uint(eci)
+		envMeta.Namespace = namespace
+		envMeta.GitHubMetadata = ghMeta
 	} else {
 		namespace = fmt.Sprintf("porter-stack-%s", applicationName)
 	}
@@ -64,31 +62,32 @@ func formatNamespaceForEnvironment(envName, repoOwner, repo, branch string) stri
 	return fmt.Sprintf("porter-env-%s-%s-%s-%s", envName, repoOwner, repo, branch)
 }
 
-func getGitDeployMeta() (*GitHubMetadata, error) {
+func getGitDeployMeta() (GitHubMetadata, error) {
+	ghMeta := GitHubMetadata{}
+
 	branchFrom := os.Getenv("PORTER_BRANCH_FROM")
 	if branchFrom == "" {
-		return nil, fmt.Errorf("PORTER_BRANCH_FROM not set")
+		return ghMeta, fmt.Errorf("PORTER_BRANCH_FROM not set")
 	}
+	ghMeta.BranchFrom = branchFrom
 
 	repoName := os.Getenv("PORTER_REPO_NAME")
 	if repoName == "" {
-		return nil, fmt.Errorf("PORTER_REPO_NAME not set")
+		return ghMeta, fmt.Errorf("PORTER_REPO_NAME not set")
 	}
+	ghMeta.Repo = repoName
 
 	repoOwner := os.Getenv("PORTER_REPO_OWNER")
 	if repoOwner == "" {
-		return nil, fmt.Errorf("PORTER_REPO_OWNER not set")
+		return ghMeta, fmt.Errorf("PORTER_REPO_OWNER not set")
 	}
+	ghMeta.RepoOwner = repoOwner
 
 	prName := os.Getenv("PORTER_PR_NAME")
 	if prName == "" {
-		return nil, fmt.Errorf("PORTER_PR_NAME not set")
+		return ghMeta, fmt.Errorf("PORTER_PR_NAME not set")
 	}
+	ghMeta.PRName = prName
 
-	return &GitHubMetadata{
-		RepoOwner:  repoOwner,
-		Repo:       repoName,
-		BranchFrom: branchFrom,
-		PRName:     prName,
-	}, nil
+	return ghMeta, nil
 }

+ 22 - 3
cli/cmd/stack/hooks.go

@@ -20,7 +20,7 @@ type DeployAppHook struct {
 	PorterYAML           []byte
 	Builder              string
 	Namespace            string
-	EnvironmentMeta      *EnvironmentMeta
+	EnvironmentMeta      EnvironmentMeta
 }
 
 func (t *DeployAppHook) PreApply() error {
@@ -88,6 +88,10 @@ func (t *DeployAppHook) applyApp(client *api.Client, shouldCreate bool, driverOu
 		Namespace:        t.Namespace,
 	}
 
+	if t.EnvironmentMeta.EnvironmentConfigID != 0 {
+		req.EnvironmentConfigID = t.EnvironmentMeta.EnvironmentConfigID
+	}
+
 	_, err := client.CreatePorterApp(
 		context.Background(),
 		t.ProjectID,
@@ -103,9 +107,24 @@ 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 {
+	if t.EnvironmentMeta.GitHubMetadata.BranchFrom != "" {
+		color.New(color.FgGreen).Printf("Creating preview environment for app %s based on branch '%s'\n", t.ApplicationName, t.EnvironmentMeta.GitHubMetadata.BranchFrom)
 		// create preview env record
-
+		_, err = client.CreatePreviewEnvironment(
+			context.Background(),
+			t.ProjectID,
+			t.ClusterID,
+			&types.CreatePreviewEnvironmentRequest{
+				EnvironmentConfigID: t.EnvironmentMeta.EnvironmentConfigID,
+				GitRepoOwner:        t.EnvironmentMeta.GitHubMetadata.RepoOwner,
+				GitRepoName:         t.EnvironmentMeta.GitHubMetadata.Repo,
+				Branch:              t.EnvironmentMeta.GitHubMetadata.BranchFrom,
+			},
+		)
+
+		if err != nil {
+			return fmt.Errorf("error creating preview environment: %w", err)
+		}
 	}
 
 	return nil

+ 1 - 0
internal/models/environment_config.go

@@ -21,6 +21,7 @@ type EnvironmentConfig struct {
 	Auto bool
 
 	PreviewEnvironments []PreviewEnvironment
+	PorterApps          []PorterApp
 }
 
 func (c *EnvironmentConfig) ToEnvironmentConfigType() *types.EnvironmentConfig {

+ 2 - 0
internal/models/porter_app.go

@@ -30,6 +30,8 @@ type PorterApp struct {
 
 	// Porter YAML
 	PorterYamlPath string
+
+	EnvironmentConfigID uint
 }
 
 // ToPorterAppType generates an external types.PorterApp to be shared over REST

+ 3 - 0
internal/models/project.go

@@ -48,6 +48,9 @@ type Project struct {
 	// provisioned aws infra
 	Infras []Infra `json:"infras"`
 
+	// configured environments
+	EnvironmentConfigs []EnvironmentConfig `json:"environment_configs"`
+
 	// auth mechanisms
 	KubeIntegrations   []ints.KubeIntegration   `json:"kube_integrations"`
 	BasicIntegrations  []ints.BasicIntegration  `json:"basic_integrations"`

+ 0 - 1
internal/repository/environment_config.go

@@ -4,7 +4,6 @@ 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)

+ 11 - 1
internal/repository/gorm/porter_app.go

@@ -37,7 +37,17 @@ func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*mo
 func (repo *PorterAppRepository) ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error) {
 	app := &models.PorterApp{}
 
-	if err := repo.db.Where("cluster_id = ? AND name = ?", clusterID, name).Limit(1).Find(&app).Error; err != nil {
+	if err := repo.db.Where("cluster_id = ? AND name = ? AND environment_config_id IS NULL", clusterID, name).Limit(1).Find(&app).Error; err != nil {
+		return nil, err
+	}
+
+	return app, nil
+}
+
+func (repo *PorterAppRepository) ReadPorterAppByNameInEnvironment(clusterID uint, name string, envConfigID uint) (*models.PorterApp, error) {
+	app := &models.PorterApp{}
+
+	if err := repo.db.Where("cluster_id = ? AND name = ? AND environment_config_id = ?", clusterID, name, envConfigID).First(&app).Error; err != nil {
 		return nil, err
 	}
 

+ 1 - 0
internal/repository/porter_app.go

@@ -7,6 +7,7 @@ import (
 // PorterAppRepository represents the set of queries on the PorterApp model
 type PorterAppRepository interface {
 	ReadPorterAppByName(clusterID uint, name string) (*models.PorterApp, error)
+	ReadPorterAppByNameInEnvironment(clusterID uint, name string, envConfigID uint) (*models.PorterApp, error)
 	CreatePorterApp(app *models.PorterApp) (*models.PorterApp, error)
 	ListPorterAppByClusterID(clusterID uint) ([]*models.PorterApp, error)
 	UpdatePorterApp(app *models.PorterApp) (*models.PorterApp, error)

+ 4 - 0
internal/repository/test/porter_app.go

@@ -36,3 +36,7 @@ func (repo *PorterAppRepository) ListPorterAppByClusterID(clusterID uint) ([]*mo
 func (repo *PorterAppRepository) DeletePorterApp(app *models.PorterApp) (*models.PorterApp, error) {
 	return nil, errors.New("cannot write database")
 }
+
+func (repo *PorterAppRepository) ReadPorterAppByNameInEnvironment(clusterID uint, name string, envConfigID uint) (*models.PorterApp, error) {
+	return nil, errors.New("cannot write database")
+}