소스 검색

add top level app fields as flags for apply (#4411)

ianedwards 2 년 전
부모
커밋
10cb1cd1c3

+ 5 - 2
api/client/porter_app.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/porter_app"
 	"github.com/porter-dev/porter/internal/models"
 	appInternal "github.com/porter-dev/porter/internal/porter_app"
+	v2 "github.com/porter-dev/porter/internal/porter_app/v2"
 
 	"github.com/porter-dev/porter/api/types"
 )
@@ -157,12 +158,14 @@ func (c *Client) ParseYAML(
 	projectID, clusterID uint,
 	b64Yaml string,
 	appName string,
+	patchOperations []v2.PatchOperation,
 ) (*porter_app.ParsePorterYAMLToProtoResponse, error) {
 	resp := &porter_app.ParsePorterYAMLToProtoResponse{}
 
 	req := &porter_app.ParsePorterYAMLToProtoRequest{
-		B64Yaml: b64Yaml,
-		AppName: appName,
+		B64Yaml:         b64Yaml,
+		AppName:         appName,
+		PatchOperations: patchOperations,
 	}
 
 	err := c.postRequest(

+ 12 - 3
api/server/handlers/porter_app/parse_yaml.go

@@ -9,6 +9,7 @@ import (
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 
 	"github.com/porter-dev/porter/internal/porter_app"
+	v2 "github.com/porter-dev/porter/internal/porter_app/v2"
 
 	"github.com/porter-dev/porter/internal/telemetry"
 
@@ -38,8 +39,9 @@ func NewParsePorterYAMLToProtoHandler(
 
 // ParsePorterYAMLToProtoRequest is the request object for the /apps/parse endpoint
 type ParsePorterYAMLToProtoRequest struct {
-	B64Yaml string `json:"b64_yaml"`
-	AppName string `json:"app_name"`
+	B64Yaml         string              `json:"b64_yaml"`
+	AppName         string              `json:"app_name"`
+	PatchOperations []v2.PatchOperation `json:"patch_operations"`
 }
 
 // EncodedAppWithEnv is a struct that contains a base64-encoded app proto object and a map of env variables
@@ -107,9 +109,16 @@ func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
+	patchedProto, err := v2.PatchApp(ctx, appDefinition.AppProto, request.PatchOperations)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error patching app proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
 	response := &ParsePorterYAMLToProtoResponse{}
 
-	encodedApp, err := encodeAppProto(ctx, appDefinition.AppProto)
+	encodedApp, err := encodeAppProto(ctx, patchedProto)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error encoding app proto")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))

+ 35 - 23
cli/cmd/commands/apply.go

@@ -13,7 +13,9 @@ import (
 	"strings"
 	"time"
 
+	"github.com/porter-dev/porter/cli/cmd/commands/flags"
 	v2 "github.com/porter-dev/porter/cli/cmd/v2"
+	appV2 "github.com/porter-dev/porter/internal/porter_app/v2"
 
 	"github.com/cli/cli/git"
 	"github.com/fatih/color"
@@ -40,9 +42,9 @@ import (
 )
 
 var (
-	porterYAML       string
-	previewApply     bool
-	imageTagOverride string
+	porterYAML   string
+	previewApply bool
+	envGroups    []string
 	// pullImageBeforeBuild is a flag that determines whether to pull the docker image from a repo before building
 	pullImageBeforeBuild bool
 	predeploy            bool
@@ -113,7 +115,6 @@ applying a configuration:
 	applyCmd.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
 	applyCmd.PersistentFlags().BoolVarP(&previewApply, "preview", "p", false, "apply as preview environment based on current git branch")
 	applyCmd.PersistentFlags().BoolVar(&pullImageBeforeBuild, "pull-before-build", false, "attempt to pull image from registry before building")
-	applyCmd.PersistentFlags().StringVar(&imageTagOverride, "tag", "", "set the image tag used for the application (overrides field in yaml)")
 	applyCmd.PersistentFlags().BoolVar(&predeploy, "predeploy", false, "run predeploy job before deploying the application")
 	applyCmd.PersistentFlags().BoolVar(&exact, "exact", false, "apply the exact configuration as specified in the porter.yaml file (default is to merge with existing configuration)")
 	applyCmd.PersistentFlags().BoolVarP(
@@ -123,6 +124,11 @@ applying a configuration:
 		false,
 		"set this to wait and be notified when an apply is successful, otherwise time out",
 	)
+	applyCmd.PersistentFlags().StringSliceVar(&envGroups, "attach-env-groups", nil, "attach environment groups to the app on apply")
+
+	flags.UseAppBuildFlags(applyCmd)
+	flags.UseAppImageFlags(applyCmd)
+
 	applyCmd.MarkFlagRequired("file")
 
 	return applyCmd
@@ -138,7 +144,7 @@ func appNameFromEnvironmentVariable() string {
 	return ""
 }
 
-func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ *cobra.Command, _ []string) (err error) {
+func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, cmd *cobra.Command, _ []string) (err error) {
 	project, err := client.GetProject(ctx, cliConfig.Project)
 	if err != nil {
 		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
@@ -146,24 +152,46 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap
 
 	appName := appNameFromEnvironmentVariable()
 
+	imageValues, err := flags.AppImageValuesFromCmd(cmd)
+	if err != nil {
+		return fmt.Errorf("could not retrieve image values from command")
+	}
+
+	buildValues, err := flags.AppBuildValuesFromCmd(cmd)
+	if err != nil {
+		return fmt.Errorf("could not retrieve build values from command")
+	}
+
 	if project.ValidateApplyV2 {
 		if previewApply && !project.PreviewEnvsEnabled {
 			return fmt.Errorf("preview environments are not enabled for this project. Please contact support@porter.run")
 		}
 
+		patchOperations := appV2.PatchOperationsFromFlagValues(appV2.PatchOperationsFromFlagValuesInput{
+			EnvGroups:       envGroups,
+			BuildMethod:     buildValues.BuildMethod,
+			Dockerfile:      buildValues.Dockerfile,
+			Builder:         buildValues.Builder,
+			Buildpacks:      buildValues.Buildpacks,
+			BuildContext:    buildValues.BuildContext,
+			ImageRepository: imageValues.Repository,
+			ImageTag:        imageValues.Tag,
+		})
+
 		inp := v2.ApplyInput{
 			CLIConfig:                   cliConfig,
 			Client:                      client,
 			PorterYamlPath:              porterYAML,
 			AppName:                     appName,
-			ImageTagOverride:            imageTagOverride,
+			ImageTagOverride:            imageValues.Tag,
 			PreviewApply:                previewApply,
 			WaitForSuccessfulDeployment: appWait,
 			PullImageBeforeBuild:        pullImageBeforeBuild,
 			WithPredeploy:               predeploy,
 			Exact:                       exact,
+			PatchOperations:             patchOperations,
 		}
-		err := v2.Apply(ctx, inp)
+		err = v2.Apply(ctx, inp)
 		if err != nil {
 			return err
 		}
@@ -180,7 +208,6 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap
 	}
 
 	err = yaml.Unmarshal(fileBytes, &previewVersion)
-
 	if err != nil {
 		return fmt.Errorf("error unmarshaling porter.yaml: %w", err)
 	}
@@ -197,7 +224,6 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap
 		}
 
 		resGroup, err = applier.DowngradeToV1()
-
 		if err != nil {
 			return err
 		}
@@ -210,7 +236,6 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap
 		}
 
 		resGroup, err = parser.ParseRawBytes(fileBytes)
-
 		if err != nil {
 			return fmt.Errorf("error parsing porter.yaml: %w", err)
 		}
@@ -265,10 +290,6 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap
 				Release:  parsed.Release,
 			}
 
-			if err != nil {
-				return fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
-			}
-
 			resources, err := porter_app.CreateApplicationDeploy(ctx, client, worker, app, appName, cliConfig)
 			if err != nil {
 				return fmt.Errorf("error parsing porter.yaml for build resources: %w", err)
@@ -519,7 +540,6 @@ func (d *DeployDriver) applyAddon(ctx context.Context, resource *switchboardMode
 				Values: string(bytes),
 			},
 		)
-
 		if err != nil {
 			return nil, fmt.Errorf("error updating addon from resource %s: %w", resource.Name, err)
 		}
@@ -601,13 +621,11 @@ func (d *DeployDriver) applyApplication(ctx context.Context, resource *switchboa
 
 	if shouldCreate {
 		resource, err = d.createApplication(ctx, resource, client, sharedOpts, appConfig)
-
 		if err != nil {
 			return nil, fmt.Errorf("error creating app from resource %s: %w", resourceName, err)
 		}
 	} else if !appConfig.OnlyCreate {
 		resource, err = d.updateApplication(ctx, resource, client, sharedOpts, appConfig)
-
 		if err != nil {
 			return nil, fmt.Errorf("error updating application from resource %s: %w", resourceName, err)
 		}
@@ -764,7 +782,6 @@ func (d *DeployDriver) createApplication(ctx context.Context, resource *switchbo
 					ImageRepoURI: imageURL,
 				},
 			)
-
 			if err != nil {
 				return nil, err
 			}
@@ -806,7 +823,6 @@ func (d *DeployDriver) updateApplication(ctx context.Context, resource *switchbo
 		}
 
 		err = updateAgent.SetBuildEnv(buildEnv)
-
 		if err != nil {
 			return nil, err
 		}
@@ -821,14 +837,12 @@ func (d *DeployDriver) updateApplication(ctx context.Context, resource *switchbo
 		}
 
 		err = updateAgent.Build(ctx, buildConfig)
-
 		if err != nil {
 			return nil, err
 		}
 
 		if !appConf.Build.UseCache {
 			err = updateAgent.Push(ctx)
-
 			if err != nil {
 				return nil, err
 			}
@@ -894,7 +908,6 @@ func (d *DeployDriver) getApplicationConfig(resource *switchboardModels.Resource
 	appConf := &previewInt.ApplicationConfig{}
 
 	err = mapstructure.Decode(populatedConf, appConf)
-
 	if err != nil {
 		return nil, err
 	}
@@ -1423,7 +1436,6 @@ func (t *CloneEnvGroupHook) PreApply() error {
 							TargetNamespace: target.Namespace,
 						},
 					)
-
 					if err != nil {
 						return err
 					}

+ 97 - 0
cli/cmd/commands/flags/app_build.go

@@ -0,0 +1,97 @@
+package flags
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+const (
+	// App_BuildMethod is the key for the build method flag
+	App_BuildMethod = "build-method"
+	// App_Dockerfile is the key for the dockerfile flag
+	App_Dockerfile = "dockerfile"
+	// App_Builder is the key for the builder flag
+	App_Builder = "builder"
+	// App_Buildpacks is the key for the buildpacks flag
+	App_Buildpacks = "attach-buildpacks"
+	// App_BuildContext is the key for the build context flag
+	App_BuildContext = "build-context"
+)
+
+// UseAppBuildFlags adds build flags to the given command
+func UseAppBuildFlags(cmd *cobra.Command) {
+	cmd.PersistentFlags().String(
+		App_BuildMethod,
+		"",
+		"set the build method for the app on apply, either 'docker' or 'pack'",
+	)
+	cmd.PersistentFlags().String(
+		App_Dockerfile,
+		"",
+		"set the path to the Dockerfile when build method is 'docker'",
+	)
+	cmd.PersistentFlags().String(
+		App_Builder,
+		"",
+		"set the builder to use when build method is 'pack'",
+	)
+	cmd.PersistentFlags().StringSlice(
+		App_Buildpacks,
+		nil,
+		"attach buildpacks to use when build method is 'pack'",
+	)
+	cmd.PersistentFlags().String(
+		App_BuildContext,
+		"",
+		"set the build context for the app",
+	)
+}
+
+type buildValues struct {
+	BuildMethod  string
+	Dockerfile   string
+	Builder      string
+	Buildpacks   []string
+	BuildContext string
+}
+
+// AppBuildValuesFromCmd retrieves build values from command flags
+func AppBuildValuesFromCmd(cmd *cobra.Command) (buildValues, error) {
+	var values buildValues
+
+	buildMethod, err := cmd.Flags().GetString(App_BuildMethod)
+	if err != nil {
+		return values, fmt.Errorf("error getting build method: %s", err)
+	}
+
+	dockerfile, err := cmd.Flags().GetString(App_Dockerfile)
+	if err != nil {
+		return values, fmt.Errorf("error getting dockerfile: %s", err)
+	}
+
+	builder, err := cmd.Flags().GetString(App_Builder)
+	if err != nil {
+		return values, fmt.Errorf("error getting builder: %s", err)
+	}
+
+	buildpacks, err := cmd.Flags().GetStringSlice(App_Buildpacks)
+	if err != nil {
+		return values, fmt.Errorf("error getting buildpacks: %s", err)
+	}
+
+	buildContext, err := cmd.Flags().GetString(App_BuildContext)
+	if err != nil {
+		return values, fmt.Errorf("error getting build context: %s", err)
+	}
+
+	values = buildValues{
+		BuildMethod:  buildMethod,
+		Dockerfile:   dockerfile,
+		Builder:      builder,
+		Buildpacks:   buildpacks,
+		BuildContext: buildContext,
+	}
+
+	return values, nil
+}

+ 55 - 0
cli/cmd/commands/flags/app_image.go

@@ -0,0 +1,55 @@
+package flags
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+)
+
+const (
+	// App_ImageTag is the key for the image tag flag
+	App_ImageTag = "tag"
+	// App_ImageRepository is the key for the image repository flag
+	App_ImageRepository = "image-repository"
+)
+
+// UseAppImageFlags adds image flags to the given command
+func UseAppImageFlags(cmd *cobra.Command) {
+	cmd.PersistentFlags().String(
+		App_ImageTag,
+		"",
+		"set the image tag used for the application (overrides field in yaml)",
+	)
+	cmd.PersistentFlags().String(
+		App_ImageRepository,
+		"",
+		"set the image repository to use for the app",
+	)
+}
+
+type imageValues struct {
+	Tag        string
+	Repository string
+}
+
+// AppImageValuesFromCmd retrieves image values from command flags
+func AppImageValuesFromCmd(cmd *cobra.Command) (imageValues, error) {
+	var values imageValues
+
+	tag, err := cmd.Flags().GetString(App_ImageTag)
+	if err != nil {
+		return values, fmt.Errorf("error getting tag: %w", err)
+	}
+
+	repo, err := cmd.Flags().GetString(App_ImageRepository)
+	if err != nil {
+		return values, fmt.Errorf("error getting repository: %w", err)
+	}
+
+	values = imageValues{
+		Tag:        tag,
+		Repository: repo,
+	}
+
+	return values, nil
+}

+ 36 - 7
cli/cmd/v2/apply.go

@@ -14,7 +14,10 @@ import (
 	"time"
 
 	"github.com/fatih/color"
-	"github.com/porter-dev/porter/api/server/handlers/porter_app"
+	app_api "github.com/porter-dev/porter/api/server/handlers/porter_app"
+	"github.com/porter-dev/porter/internal/porter_app"
+	v2 "github.com/porter-dev/porter/internal/porter_app/v2"
+	"gopkg.in/yaml.v3"
 
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -47,6 +50,8 @@ type ApplyInput struct {
 	WithPredeploy bool
 	// Exact is true when Apply should use the exact app config provided by the user
 	Exact bool
+	// PatchOperations is a list of patch operations to apply to the app
+	PatchOperations []v2.PatchOperation
 }
 
 // Apply implements the functionality of the `porter apply` command for validate apply v2 projects
@@ -116,12 +121,36 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 		color.New(color.FgGreen).Printf("Using Porter YAML at path: %s\n", inp.PorterYamlPath) // nolint:errcheck,gosec
 	}
 
+	if b64YAML == "" {
+		color.New(color.FgGreen).Printf("No Porter YAML found, using default configuration...\n") // nolint:errcheck,gosec
+		if inp.AppName == "" {
+			return errors.New("no porter yaml found and app name not specified")
+		}
+
+		app := v2.PorterApp{
+			Version: string(porter_app.PorterYamlVersion_V2),
+			Name:    inp.AppName,
+		}
+
+		by, err := yaml.Marshal(app)
+		if err != nil {
+			return fmt.Errorf("error marshaling default porter yaml: %w", err)
+		}
+
+		b64YAML = base64.StdEncoding.EncodeToString(by)
+	}
+
 	commitSHA := commitSHAFromEnv()
 	gitSource, err := gitSourceFromEnv()
 	if err != nil {
 		return fmt.Errorf("error getting git source from env: %w", err)
 	}
 
+	parseRes, err := client.ParseYAML(ctx, cliConf.Project, cliConf.Cluster, b64YAML, inp.AppName, inp.PatchOperations)
+	if err != nil {
+		return fmt.Errorf("error parsing porter yaml: %w", err)
+	}
+
 	updateInput := api.UpdateAppInput{
 		ProjectID:          cliConf.Project,
 		ClusterID:          cliConf.Cluster,
@@ -130,7 +159,7 @@ func Apply(ctx context.Context, inp ApplyInput) error {
 		GitSource:          gitSource,
 		DeploymentTargetId: deploymentTargetID,
 		CommitSHA:          commitSHA,
-		Base64PorterYAML:   b64YAML,
+		Base64AppProto:     parseRes.B64AppProto,
 		WithPredeploy:      inp.WithPredeploy,
 		Exact:              inp.Exact,
 	}
@@ -428,8 +457,8 @@ const checkDeployTimeout = 15 * time.Minute
 // checkDeployFrequency is the frequency for checking if an app has been deployed
 const checkDeployFrequency = 10 * time.Second
 
-func gitSourceFromEnv() (porter_app.GitSource, error) {
-	var source porter_app.GitSource
+func gitSourceFromEnv() (app_api.GitSource, error) {
+	var source app_api.GitSource
 
 	var repoID uint
 	if os.Getenv("GITHUB_REPOSITORY_ID") != "" {
@@ -440,7 +469,7 @@ func gitSourceFromEnv() (porter_app.GitSource, error) {
 		repoID = uint(id)
 	}
 
-	return porter_app.GitSource{
+	return app_api.GitSource{
 		GitBranch:   os.Getenv("GITHUB_REF_NAME"),
 		GitRepoID:   repoID,
 		GitRepoName: os.Getenv("GITHUB_REPOSITORY"),
@@ -451,8 +480,8 @@ type buildInputFromBuildSettingsInput struct {
 	projectID            uint
 	appName              string
 	commitSHA            string
-	image                porter_app.Image
-	build                porter_app.BuildSettings
+	image                app_api.Image
+	build                app_api.BuildSettings
 	buildEnv             map[string]string
 	pullImageBeforeBuild bool
 }

+ 2 - 1
go.mod

@@ -74,6 +74,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0
 	github.com/briandowns/spinner v1.18.1
 	github.com/cloudflare/cloudflare-go v0.76.0
+	github.com/evanphx/json-patch/v5 v5.9.0
 	github.com/glebarez/sqlite v1.6.0
 	github.com/go-chi/chi/v5 v5.0.8
 	github.com/gosimple/slug v1.13.1
@@ -129,6 +130,7 @@ require (
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f // indirect
 	github.com/emicklei/go-restful/v3 v3.9.0 // indirect
+	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
 	github.com/felixge/httpsnoop v1.0.2 // indirect
 	github.com/glebarez/go-sqlite v1.20.0 // indirect
 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect
@@ -228,7 +230,6 @@ require (
 	github.com/docker/go-units v0.4.0 // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
-	github.com/evanphx/json-patch v5.9.0+incompatible
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/gdamore/encoding v1.0.0 // indirect

+ 4 - 4
go.sum

@@ -586,8 +586,10 @@ github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHj
 github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
-github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
+github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
+github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
 github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
@@ -1523,8 +1525,6 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.2.123 h1:bDtyC2ueirKmu9NN1YEClv2qVrMjvu913HGibG7ISRQ=
-github.com/porter-dev/api-contracts v0.2.123/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/api-contracts v0.2.124 h1:0ChXriR88KanBMMJfDWIabEvPqt9eLsmOScDbuJucBQ=
 github.com/porter-dev/api-contracts v0.2.124/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=

+ 7 - 7
internal/porter_app/v2/apply_flags.go

@@ -56,7 +56,7 @@ type SetName struct {
 func (f SetName) AsPatchOperations() []PatchOperation {
 	return []PatchOperation{
 		{
-			Operation: ReplaceOperation,
+			Operation: AddOperation,
 			Path:      "/name",
 			Value:     f.Name,
 		},
@@ -106,7 +106,7 @@ type SetBuildContext struct {
 func (f SetBuildContext) AsPatchOperations() []PatchOperation {
 	return []PatchOperation{
 		{
-			Operation: ReplaceOperation,
+			Operation: AddOperation,
 			Path:      "/build/context",
 			Value:     f.Context,
 		},
@@ -122,7 +122,7 @@ type SetBuildMethod struct {
 func (f SetBuildMethod) AsPatchOperations() []PatchOperation {
 	return []PatchOperation{
 		{
-			Operation: ReplaceOperation,
+			Operation: AddOperation,
 			Path:      "/build/method",
 			Value:     f.Method,
 		},
@@ -138,7 +138,7 @@ type SetBuildDockerfile struct {
 func (f SetBuildDockerfile) AsPatchOperations() []PatchOperation {
 	return []PatchOperation{
 		{
-			Operation: ReplaceOperation,
+			Operation: AddOperation,
 			Path:      "/build/dockerfile",
 			Value:     f.Dockerfile,
 		},
@@ -174,7 +174,7 @@ type SetBuilder struct {
 func (f SetBuilder) AsPatchOperations() []PatchOperation {
 	return []PatchOperation{
 		{
-			Operation: ReplaceOperation,
+			Operation: AddOperation,
 			Path:      "/build/builder",
 			Value:     f.Builder,
 		},
@@ -190,7 +190,7 @@ type SetImageRepo struct {
 func (f SetImageRepo) AsPatchOperations() []PatchOperation {
 	return []PatchOperation{
 		{
-			Operation: ReplaceOperation,
+			Operation: AddOperation,
 			Path:      "/image/repository",
 			Value:     f.Repo,
 		},
@@ -206,7 +206,7 @@ type SetImageTag struct {
 func (f SetImageTag) AsPatchOperations() []PatchOperation {
 	return []PatchOperation{
 		{
-			Operation: ReplaceOperation,
+			Operation: AddOperation,
 			Path:      "/image/tag",
 			Value:     f.Tag,
 		},

+ 77 - 2
internal/porter_app/v2/patch.go

@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"strings"
 
-	jsonpatch "github.com/evanphx/json-patch"
+	jsonpatch "github.com/evanphx/json-patch/v5"
 	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/internal/telemetry"
@@ -46,7 +46,9 @@ func PatchApp(ctx context.Context, app *porterv1.PorterApp, ops []PatchOperation
 		return patchedApp, telemetry.Error(ctx, span, err, "failed to decode patch")
 	}
 
-	modified, err := patch.Apply(by)
+	modified, err := patch.ApplyWithOptions(by, &jsonpatch.ApplyOptions{
+		EnsurePathExistsOnAdd: true,
+	})
 	if err != nil {
 		return patchedApp, telemetry.Error(ctx, span, err, "failed to apply patch")
 	}
@@ -60,3 +62,76 @@ func PatchApp(ctx context.Context, app *porterv1.PorterApp, ops []PatchOperation
 
 	return patchedApp, nil
 }
+
+// PatchOperationsFromFlagValuesInput is the input for PatchOperationsFromFlagValues
+type PatchOperationsFromFlagValuesInput struct {
+	EnvGroups       []string
+	BuildMethod     string
+	Dockerfile      string
+	Builder         string
+	Buildpacks      []string
+	BuildContext    string
+	ImageRepository string
+	ImageTag        string
+}
+
+// PatchOperationsFromFlagValues converts the flag values into a list of patch operations
+func PatchOperationsFromFlagValues(inp PatchOperationsFromFlagValuesInput) []PatchOperation {
+	var ops []PatchOperation
+
+	var flags []ApplyFlag
+
+	if inp.EnvGroups != nil {
+		flags = append(flags, AttachEnvGroupsFlag{
+			EnvGroups: inp.EnvGroups,
+		})
+	}
+
+	if inp.BuildMethod != "" {
+		flags = append(flags, SetBuildMethod{
+			Method: inp.BuildMethod,
+		})
+	}
+
+	if inp.Dockerfile != "" {
+		flags = append(flags, SetBuildDockerfile{
+			Dockerfile: inp.Dockerfile,
+		})
+	}
+
+	if inp.Builder != "" {
+		flags = append(flags, SetBuilder{
+			Builder: inp.Builder,
+		})
+	}
+
+	if inp.Buildpacks != nil {
+		flags = append(flags, AttachBuildpacks{
+			Buildpacks: inp.Buildpacks,
+		})
+	}
+
+	if inp.BuildContext != "" {
+		flags = append(flags, SetBuildContext{
+			Context: inp.BuildContext,
+		})
+	}
+
+	if inp.ImageRepository != "" {
+		flags = append(flags, SetImageRepo{
+			Repo: inp.ImageRepository,
+		})
+	}
+
+	if inp.ImageTag != "" {
+		flags = append(flags, SetImageTag{
+			Tag: inp.ImageTag,
+		})
+	}
+
+	for _, flag := range flags {
+		ops = append(ops, flag.AsPatchOperations()...)
+	}
+
+	return ops
+}