Ver Fonte

Update CLI to use feature flags, instead of project calls (#3451)

Stefan McShane há 2 anos atrás
pai
commit
478fee1b6b

+ 0 - 29
api/client/api.go

@@ -10,13 +10,11 @@ import (
 	"net/http"
 	"net/url"
 	"os"
-	"path/filepath"
 	"strings"
 	"time"
 
 	"github.com/gorilla/schema"
 	"github.com/porter-dev/porter/api/types"
-	"k8s.io/client-go/util/homedir"
 )
 
 // Client represents the client for the Porter API
@@ -82,33 +80,6 @@ func NewClientWithConfig(ctx context.Context, input NewClientInput) (Client, err
 // ErrNoAuthCredential returns an error when no auth credentials have been provided such as cookies or tokens
 var ErrNoAuthCredential = errors.New("unable to create an API session with cookie nor token")
 
-// NewClient constructs a new client based on a set of options
-func NewClient(baseURL string, cookieFileName string) *Client {
-	home := homedir.HomeDir()
-	cookieFilePath := filepath.Join(home, ".porter", cookieFileName)
-
-	client := &Client{
-		BaseURL:        baseURL,
-		CookieFilePath: cookieFilePath,
-		HTTPClient: &http.Client{
-			Timeout: time.Minute,
-		},
-	}
-
-	cookie, _ := client.getCookie()
-
-	if cookie != nil {
-		client.Cookie = cookie
-	}
-
-	// look for a cloudflare access token specifically for Porter
-	if cfToken := os.Getenv("PORTER_CF_ACCESS_TOKEN"); cfToken != "" {
-		client.cfToken = cfToken
-	}
-
-	return client
-}
-
 func (c *Client) getRequest(relPath string, data interface{}, response interface{}) error {
 	vals := make(map[string][]string)
 	err := schema.NewEncoder().Encode(data, vals)

+ 66 - 0
cli/cmd/commands/all.go

@@ -50,3 +50,69 @@ func RegisterCommands() (*cobra.Command, error) {
 	rootCmd.AddCommand(registerCommand_Version(cliConf))
 	return rootCmd, nil
 }
+
+// overrideConfigWithFlags grabs the runtime value of registered flags, and overrides the values in CLIConfig.
+// It was done this way to reduce the size of a refactor, as the codebase conflates initialisation of the commands, with the runtime values.
+func overrideConfigWithFlags(cmd *cobra.Command, config config.CLIConfig) config.CLIConfig {
+	type flag struct {
+		// stringName is the name of the flag which is a string
+		stringName string
+		// stringConfigTarget is the pointer to the string in the config struct
+		stringConfigTarget *string
+
+		// uintName is the name of the flag which is a uint
+		uintName string
+		// uintConfigTarget is the pointer to the uint in the config struct
+		uintConfigTarget *uint
+	}
+
+	flagsToOverride := []flag{
+		{
+			stringName:         "driver",
+			stringConfigTarget: &config.Driver,
+		},
+		{
+			stringName:         "host",
+			stringConfigTarget: &config.Host,
+		},
+		{
+			stringName:         "token",
+			stringConfigTarget: &config.Token,
+		},
+		{
+			stringName:         "kubeconfig",
+			stringConfigTarget: &config.Kubeconfig,
+		},
+		{
+			uintName:         "project",
+			uintConfigTarget: &config.Project,
+		},
+		{
+			uintName:         "cluster",
+			uintConfigTarget: &config.Cluster,
+		},
+		{
+			uintName:         "registry",
+			uintConfigTarget: &config.Registry,
+		},
+		{
+			uintName:         "helm_repo",
+			uintConfigTarget: &config.HelmRepo,
+		},
+	}
+	for _, fl := range flagsToOverride {
+		if fl.stringName != "" {
+			st, _ := cmd.Flags().GetString(fl.stringName)
+			if st != "" {
+				*fl.stringConfigTarget = st
+			}
+		}
+		if fl.uintName != "" {
+			ui, _ := cmd.Flags().GetUint(fl.uintName)
+			if ui != 0 {
+				*fl.uintConfigTarget = ui
+			}
+		}
+	}
+	return config
+}

+ 6 - 6
cli/cmd/commands/app.go

@@ -55,7 +55,7 @@ func registerCommand_App(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.MinimumNArgs(2),
 		Short: "Runs a command inside a connected cluster container.",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, appRun)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, appRun)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -70,7 +70,7 @@ func registerCommand_App(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.NoArgs,
 		Short: "Delete any lingering ephemeral pods that were created with \"porter app run\".",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, appCleanup)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, appCleanup)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -84,7 +84,7 @@ func registerCommand_App(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.MinimumNArgs(1),
 		Short: "Updates the image tag for an application.",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, appUpdateTag)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, appUpdateTag)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -152,7 +152,7 @@ func appRunFlags(appRunCmd *cobra.Command) {
 	)
 }
 
-func appRun(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
+func appRun(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, args []string) error {
 	execArgs := args[1:]
 
 	color.New(color.FgGreen).Println("Attempting to run", strings.Join(execArgs, " "), "for application", args[0])
@@ -267,7 +267,7 @@ func appRun(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client a
 	return appExecuteRunEphemeral(ctx, config, appNamespace, selectedPod.Name, selectedContainerName, execArgs)
 }
 
-func appCleanup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ []string) error {
+func appCleanup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	config := &AppPorterRunSharedConfig{
 		Client:    client,
 		CLIConfig: cliConfig,
@@ -1044,7 +1044,7 @@ func appCreateEphemeralPodFromExisting(
 	)
 }
 
-func appUpdateTag(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
+func appUpdateTag(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, args []string) error {
 	namespace := fmt.Sprintf("porter-stack-%s", args[0])
 	if appTag == "" {
 		appTag = "latest"

+ 2 - 2
cli/cmd/commands/apply.go

@@ -73,7 +73,7 @@ applying a configuration:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter apply -f porter.yaml"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, apply)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, apply)
 			if err != nil {
 				if strings.Contains(err.Error(), "Forbidden") {
 					_, _ = color.New(color.FgRed).Fprintf(os.Stderr, "You may have to update your GitHub secret token")
@@ -108,7 +108,7 @@ applying a configuration:
 	return applyCmd
 }
 
-func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ []string) (err error) {
+func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ []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")

+ 16 - 8
cli/cmd/commands/auth.go

@@ -29,9 +29,14 @@ func registerCommand_Auth(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "login",
 		Short: "Authorizes a user for a given Porter server",
 		Run: func(cmd *cobra.Command, args []string) {
+			cliConf = overrideConfigWithFlags(cmd, cliConf)
+
 			err := login(cmd.Context(), cliConf)
 			if err != nil {
 				color.Red("Error logging in: %s\n", err.Error())
+				if strings.Contains(err.Error(), "Forbidden") {
+					_ = cliConf.SetToken("")
+				}
 				os.Exit(1)
 			}
 		},
@@ -41,6 +46,8 @@ func registerCommand_Auth(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "register",
 		Short: "Creates a user for a given Porter server",
 		Run: func(cmd *cobra.Command, args []string) {
+			cliConf = overrideConfigWithFlags(cmd, cliConf)
+
 			err := register(cmd.Context(), cliConf)
 			if err != nil {
 				color.Red("Error registering: %s\n", err.Error())
@@ -53,8 +60,9 @@ func registerCommand_Auth(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "logout",
 		Short: "Logs a user out of a given Porter server",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, logout)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, logout)
 			if err != nil {
+				_ = cliConf.SetToken("")
 				os.Exit(1)
 			}
 		},
@@ -88,9 +96,8 @@ func login(ctx context.Context, cliConf config.CLIConfig) error {
 	user, err := client.AuthCheck(ctx)
 	if err != nil {
 		if !strings.Contains(err.Error(), "Forbidden") {
-			return fmt.Errorf("unexpected error performing authorization check")
+			return fmt.Errorf("unexpected error performing authorization check: %w", err)
 		}
-		fmt.Println(err)
 	}
 
 	if cliConf.Token == "" {
@@ -107,7 +114,6 @@ func login(ctx context.Context, cliConf config.CLIConfig) error {
 
 		// set the token in config
 		err = cliConf.SetToken(token)
-
 		if err != nil {
 			return err
 		}
@@ -121,7 +127,6 @@ func login(ctx context.Context, cliConf config.CLIConfig) error {
 		}
 
 		user, err = client.AuthCheck(ctx)
-
 		if err != nil {
 			color.Red("Invalid token.")
 			return err
@@ -137,7 +142,6 @@ func login(ctx context.Context, cliConf config.CLIConfig) error {
 	if err != nil {
 		return err
 	}
-	_, _ = color.New(color.FgGreen).Println("Successfully logged in!")
 
 	projID, exists, err := api.GetProjectIDFromToken(cliConf.Token)
 	if err != nil {
@@ -167,6 +171,8 @@ func login(ctx context.Context, cliConf config.CLIConfig) error {
 			return err
 		}
 	}
+	_, _ = color.New(color.FgGreen).Println("Successfully logged in!")
+
 	return nil
 }
 
@@ -279,10 +285,12 @@ func register(ctx context.Context, cliConf config.CLIConfig) error {
 	return nil
 }
 
-func logout(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func logout(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	err := client.Logout(ctx)
 	if err != nil {
-		return err
+		if !strings.Contains(err.Error(), "You are not logged in.") {
+			return err
+		}
 	}
 
 	cliConf.SetToken("")

+ 6 - 6
cli/cmd/commands/cluster.go

@@ -27,7 +27,7 @@ func registerCommand_Cluster(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "list",
 		Short: "Lists the linked clusters in the current project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listClusters)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listClusters)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -40,7 +40,7 @@ func registerCommand_Cluster(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Deletes the cluster with the given id",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteCluster)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteCluster)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -59,7 +59,7 @@ func registerCommand_Cluster(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "list",
 		Short: "Lists the namespaces in a cluster",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listNamespaces)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listNamespaces)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -70,7 +70,7 @@ func registerCommand_Cluster(cliConf config.CLIConfig) *cobra.Command {
 	return clusterCmd
 }
 
-func listClusters(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listClusters(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	resp, err := client.ListProjectClusters(ctx, cliConf.Project)
 	if err != nil {
 		return err
@@ -98,7 +98,7 @@ func listClusters(ctx context.Context, user *types.GetAuthenticatedUserResponse,
 	return nil
 }
 
-func deleteCluster(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func deleteCluster(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	userResp, err := utils.PromptPlaintext(
 		fmt.Sprintf(
 			`Are you sure you'd like to delete the cluster with id %s? %s `,
@@ -128,7 +128,7 @@ func deleteCluster(ctx context.Context, user *types.GetAuthenticatedUserResponse
 	return nil
 }
 
-func listNamespaces(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listNamespaces(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	pID := cliConf.Project
 
 	// get the service account based on the cluster id

+ 6 - 6
cli/cmd/commands/config.go

@@ -46,7 +46,7 @@ func registerCommand_Config(cliConf config.CLIConfig) *cobra.Command {
 			}
 
 			if len(args) == 0 {
-				err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAndSetProject)
+				err := checkLoginAndRunWithConfig(cmd, cliConf, args, listAndSetProject)
 				if err != nil {
 					os.Exit(1)
 				}
@@ -72,7 +72,7 @@ func registerCommand_Config(cliConf config.CLIConfig) *cobra.Command {
 		Short: "Saves the cluster id in the default configuration",
 		Run: func(cmd *cobra.Command, args []string) {
 			if len(args) == 0 {
-				err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAndSetCluster)
+				err := checkLoginAndRunWithConfig(cmd, cliConf, args, listAndSetCluster)
 				if err != nil {
 					os.Exit(1)
 				}
@@ -99,7 +99,7 @@ func registerCommand_Config(cliConf config.CLIConfig) *cobra.Command {
 		Short: "Saves the registry id in the default configuration",
 		Run: func(cmd *cobra.Command, args []string) {
 			if len(args) == 0 {
-				err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAndSetRegistry)
+				err := checkLoginAndRunWithConfig(cmd, cliConf, args, listAndSetRegistry)
 				if err != nil {
 					os.Exit(1)
 				}
@@ -186,7 +186,7 @@ func printConfig() error {
 	return nil
 }
 
-func listAndSetProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listAndSetProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
 	_ = s.Color("cyan")
 	s.Suffix = " Loading list of projects"
@@ -230,7 +230,7 @@ func listAndSetProject(ctx context.Context, _ *types.GetAuthenticatedUserRespons
 	return nil
 }
 
-func listAndSetCluster(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listAndSetCluster(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
 	_ = s.Color("cyan")
 	s.Suffix = " Loading list of clusters"
@@ -273,7 +273,7 @@ func listAndSetCluster(ctx context.Context, _ *types.GetAuthenticatedUserRespons
 	return nil
 }
 
-func listAndSetRegistry(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listAndSetRegistry(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	s := spinner.New(spinner.CharSets[9], 100*time.Millisecond)
 	_ = s.Color("cyan")
 	s.Suffix = " Loading list of registries"

+ 16 - 16
cli/cmd/commands/connect.go

@@ -27,7 +27,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "kubeconfig",
 		Short: "Uses the local kubeconfig to add a cluster",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectKubeconfig)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runConnectKubeconfig)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -38,7 +38,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "ecr",
 		Short: "Adds an ECR instance to a project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectECR)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runConnectECR)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -49,7 +49,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "dockerhub",
 		Short: "Adds a Docker Hub registry integration to a project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectDockerhub)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runConnectDockerhub)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -60,7 +60,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "registry",
 		Short: "Adds a custom image registry to a project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectRegistry)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runConnectRegistry)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -71,7 +71,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "helm",
 		Short: "Adds a custom Helm registry to a project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectHelmRepo)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runConnectHelmRepo)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -82,7 +82,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "gcr",
 		Short: "Adds a GCR instance to a project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectGCR)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runConnectGCR)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -93,7 +93,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "gar",
 		Short: "Adds a GAR instance to a project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectGAR)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runConnectGAR)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -105,7 +105,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "docr",
 		Short: "Adds a DOCR instance to a project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runConnectDOCR)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runConnectDOCR)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -138,7 +138,7 @@ func registerCommand_Connect(cliConf config.CLIConfig) *cobra.Command {
 	return connectCmd
 }
 
-func runConnectKubeconfig(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+func runConnectKubeconfig(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	isLocal := false
 
 	if cliConf.Driver == "local" {
@@ -160,7 +160,7 @@ func runConnectKubeconfig(ctx context.Context, _ *types.GetAuthenticatedUserResp
 	return cliConf.SetCluster(id)
 }
 
-func runConnectECR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+func runConnectECR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	regID, err := connect.ECR(
 		ctx,
 		client,
@@ -173,7 +173,7 @@ func runConnectECR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, c
 	return cliConf.SetRegistry(regID)
 }
 
-func runConnectGCR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+func runConnectGCR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	regID, err := connect.GCR(
 		ctx,
 		client,
@@ -186,7 +186,7 @@ func runConnectGCR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, c
 	return cliConf.SetRegistry(regID)
 }
 
-func runConnectGAR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+func runConnectGAR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	regID, err := connect.GAR(
 		ctx,
 		client,
@@ -199,7 +199,7 @@ func runConnectGAR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, c
 	return cliConf.SetRegistry(regID)
 }
 
-func runConnectDOCR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+func runConnectDOCR(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	regID, err := connect.DOCR(
 		ctx,
 		client,
@@ -212,7 +212,7 @@ func runConnectDOCR(ctx context.Context, _ *types.GetAuthenticatedUserResponse,
 	return cliConf.SetRegistry(regID)
 }
 
-func runConnectDockerhub(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+func runConnectDockerhub(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	regID, err := connect.Dockerhub(
 		ctx,
 		client,
@@ -225,7 +225,7 @@ func runConnectDockerhub(ctx context.Context, _ *types.GetAuthenticatedUserRespo
 	return cliConf.SetRegistry(regID)
 }
 
-func runConnectRegistry(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+func runConnectRegistry(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	regID, err := connect.Registry(
 		ctx,
 		client,
@@ -238,7 +238,7 @@ func runConnectRegistry(ctx context.Context, _ *types.GetAuthenticatedUserRespon
 	return cliConf.SetRegistry(regID)
 }
 
-func runConnectHelmRepo(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ []string) error {
+func runConnectHelmRepo(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	hrID, err := connect.HelmRepo(
 		ctx,
 		client,

+ 5 - 10
cli/cmd/commands/create.go

@@ -78,7 +78,7 @@ To deploy an application from a Docker registry, use "--source registry" and pas
 			color.New(color.FgGreen, color.Bold).Sprintf("porter create web --app example-app --source registry --image gcr.io/snowflake-12345/example-app:latest"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, createFull)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, createFull)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -180,14 +180,9 @@ To deploy an application from a Docker registry, use "--source registry" and pas
 
 var supportedKinds = map[string]string{"web": "", "job": "", "worker": ""}
 
-func createFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.CreateFull(ctx)
+func createFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.CreateFull(ctx)
 		if err != nil {
 			return err
 		}
@@ -279,7 +274,7 @@ func createFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, clie
 				return err
 			}
 
-			err = config.SetDockerConfig(ctx, createAgent.Client, project.ID)
+			err = config.SetDockerConfig(ctx, createAgent.Client, cliConf.Project)
 
 			if err != nil {
 				return err

+ 16 - 31
cli/cmd/commands/delete.go

@@ -35,7 +35,7 @@ deleting a configuration:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter delete"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteDeployment)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteDeployment)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -49,7 +49,7 @@ deleting a configuration:
 		Short:   "Deletes an existing app",
 		Args:    cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteApp)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteApp)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -63,7 +63,7 @@ deleting a configuration:
 		Short:   "Deletes an existing job",
 		Args:    cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteJob)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteJob)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -77,7 +77,7 @@ deleting a configuration:
 		Short:   "Deletes an existing addon",
 		Args:    cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteAddon)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteAddon)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -91,7 +91,7 @@ deleting a configuration:
 		Short:   "Deletes an existing helm repo",
 		Args:    cobra.ExactArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteHelm)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteHelm)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -113,14 +113,9 @@ deleting a configuration:
 	return deleteCmd
 }
 
-func deleteDeployment(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.DeleteDeployment(ctx)
+func deleteDeployment(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.DeleteDeployment(ctx)
 		if err != nil {
 			return err
 		}
@@ -157,14 +152,9 @@ func deleteDeployment(ctx context.Context, _ *types.GetAuthenticatedUserResponse
 	)
 }
 
-func deleteApp(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.DeleteApp(ctx)
+func deleteApp(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.DeleteApp(ctx)
 		if err != nil {
 			return err
 		}
@@ -199,14 +189,9 @@ func deleteApp(ctx context.Context, _ *types.GetAuthenticatedUserResponse, clien
 	return nil
 }
 
-func deleteJob(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.DeleteJob(ctx)
+func deleteJob(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.DeleteJob(ctx)
 		if err != nil {
 			return err
 		}
@@ -241,7 +226,7 @@ func deleteJob(ctx context.Context, _ *types.GetAuthenticatedUserResponse, clien
 	return nil
 }
 
-func deleteAddon(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func deleteAddon(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	name := args[0]
 
 	resp, err := client.GetRelease(
@@ -270,7 +255,7 @@ func deleteAddon(ctx context.Context, _ *types.GetAuthenticatedUserResponse, cli
 	return nil
 }
 
-func deleteHelm(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func deleteHelm(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	name := args[0]
 
 	resp, err := client.ListHelmRepos(ctx, cliConf.Project)

+ 2 - 2
cli/cmd/commands/deploy_bluegreen.go

@@ -28,7 +28,7 @@ func registerCommand_Deploy(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "blue-green-switch",
 		Short: "Automatically switches the traffic of a blue-green deployment once the new application is ready.",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, bluegreenSwitch)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, bluegreenSwitch)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -61,7 +61,7 @@ func registerCommand_Deploy(cliConf config.CLIConfig) *cobra.Command {
 	return deployCmd
 }
 
-func bluegreenSwitch(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
+func bluegreenSwitch(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, args []string) 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")

+ 2 - 2
cli/cmd/commands/docker.go

@@ -20,7 +20,7 @@ func registerCommand_Docker(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "configure",
 		Short: "Configures the host's Docker instance",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, dockerConfig)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, dockerConfig)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -31,6 +31,6 @@ func registerCommand_Docker(cliConf config.CLIConfig) *cobra.Command {
 	return dockerCmd
 }
 
-func dockerConfig(ctx context.Context, user *ptypes.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func dockerConfig(ctx context.Context, user *ptypes.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	return config.SetDockerConfig(ctx, client, cliConf.Project)
 }

+ 19 - 2
cli/cmd/commands/errors.go

@@ -12,6 +12,7 @@ import (
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/cli/cmd/config"
 	cliErrors "github.com/porter-dev/porter/cli/cmd/errors"
+	"github.com/spf13/cobra"
 )
 
 var (
@@ -19,7 +20,12 @@ var (
 	ErrCannotConnect error = errors.New("Unable to connect to the Porter server.")
 )
 
-func checkLoginAndRunWithConfig(ctx context.Context, cliConf config.CLIConfig, args []string, runner func(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error) error {
+type authenticatedRunnerFunc func(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error
+
+func checkLoginAndRunWithConfig(cmd *cobra.Command, cliConf config.CLIConfig, args []string, runner authenticatedRunnerFunc) error {
+	ctx := cmd.Context()
+	cliConf = overrideConfigWithFlags(cmd, cliConf)
+
 	client, err := api.NewClientWithConfig(ctx, api.NewClientInput{
 		BaseURL:        fmt.Sprintf("%s/api", cliConf.Host),
 		BearerToken:    cliConf.Token,
@@ -47,8 +53,19 @@ func checkLoginAndRunWithConfig(ctx context.Context, cliConf config.CLIConfig, a
 		return err
 	}
 
-	err = runner(ctx, user, client, cliConf, args)
+	project, err := client.GetProject(ctx, cliConf.Project)
+	if err != nil {
+		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run: %w", err)
+	}
+	if project == nil {
+		return fmt.Errorf("project [%d] not found", cliConf.Project)
+	}
+
+	flags := config.FeatureFlags{
+		ValidateApplyV2Enabled: project.ValidateApplyV2,
+	}
 
+	err = runner(ctx, user, client, cliConf, flags, args)
 	if err != nil {
 		red := color.New(color.FgRed)
 

+ 8 - 18
cli/cmd/commands/get.go

@@ -24,7 +24,7 @@ func registerCommand_Get(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Fetches a release.",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, get)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, get)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -37,7 +37,7 @@ func registerCommand_Get(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Fetches the Helm values for a release.",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, getValues)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, getValues)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -71,14 +71,9 @@ type getReleaseInfo struct {
 	RevisionID   int       `json:"revision_id" yaml:"revision_id"`
 }
 
-func get(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.Get(ctx)
+func get(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.Get(ctx)
 		if err != nil {
 			return err
 		}
@@ -123,14 +118,9 @@ func get(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.
 	return nil
 }
 
-func getValues(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.GetValues(ctx)
+func getValues(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.GetValues(ctx)
 		if err != nil {
 			return err
 		}

+ 2 - 2
cli/cmd/commands/helm.go

@@ -17,7 +17,7 @@ func registerCommand_Helm(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "helm",
 		Short: "Use helm to interact with a Porter cluster",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runHelm)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runHelm)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -27,7 +27,7 @@ func registerCommand_Helm(cliConf config.CLIConfig) *cobra.Command {
 	return helmCmd
 }
 
-func runHelm(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func runHelm(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	_, err := exec.LookPath("helm")
 	if err != nil {
 		return fmt.Errorf("error finding helm: %w", err)

+ 14 - 30
cli/cmd/commands/job.go

@@ -47,7 +47,7 @@ use the --namespace flag:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter job update-images --namespace custom-namespace --image-repo-uri my-image.registry.io --tag newtag"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, batchImageUpdate)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, batchImageUpdate)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -77,7 +77,7 @@ use the --namespace flag:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter job wait --name job-example --namespace custom-namespace"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, waitForJob)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, waitForJob)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -107,7 +107,7 @@ use the --namespace flag:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter job run --name job-example --namespace custom-namespace"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runJob)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runJob)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -177,14 +177,9 @@ use the --namespace flag:
 	return jobCmd
 }
 
-func batchImageUpdate(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.BatchImageUpdate(ctx)
+func batchImageUpdate(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.BatchImageUpdate(ctx)
 		if err != nil {
 			return err
 		}
@@ -206,14 +201,9 @@ func batchImageUpdate(ctx context.Context, _ *types.GetAuthenticatedUserResponse
 }
 
 // waits for a job with a given name/namespace
-func waitForJob(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.WaitForJob(ctx)
+func waitForJob(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.WaitForJob(ctx)
 		if err != nil {
 			return err
 		}
@@ -228,14 +218,9 @@ func waitForJob(ctx context.Context, _ *types.GetAuthenticatedUserResponse, clie
 	})
 }
 
-func runJob(ctx context.Context, authRes *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.RunJob(ctx)
+func runJob(ctx context.Context, authRes *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.RunJob(ctx)
 		if err != nil {
 			return err
 		}
@@ -258,7 +243,7 @@ func runJob(ctx context.Context, authRes *types.GetAuthenticatedUserResponse, cl
 		},
 	}
 
-	err = updateAgent.UpdateImageAndValues(
+	err := updateAgent.UpdateImageAndValues(
 		ctx,
 		map[string]interface{}{
 			"paused": false,
@@ -267,8 +252,7 @@ func runJob(ctx context.Context, authRes *types.GetAuthenticatedUserResponse, cl
 		return fmt.Errorf("error running job: %w", err)
 	}
 
-	err = waitForJob(ctx, authRes, client, cliConf, args)
-
+	err = waitForJob(ctx, authRes, client, cliConf, featureFlags, args)
 	if err != nil {
 		return fmt.Errorf("error waiting for job to complete: %w", err)
 	}

+ 2 - 2
cli/cmd/commands/kubectl.go

@@ -17,7 +17,7 @@ func registerCommand_Kubectl(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "kubectl",
 		Short: "Use kubectl to interact with a Porter cluster",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, runKubectl)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, runKubectl)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -26,7 +26,7 @@ func registerCommand_Kubectl(cliConf config.CLIConfig) *cobra.Command {
 	return kubectlCmd
 }
 
-func runKubectl(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func runKubectl(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	_, err := exec.LookPath("kubectl")
 	if err != nil {
 		return fmt.Errorf("error finding kubectl: %w", err)

+ 17 - 32
cli/cmd/commands/list.go

@@ -24,7 +24,7 @@ func registerCommand_List(cliConf config.CLIConfig) *cobra.Command {
 		Short: "List applications, addons or jobs.",
 		Run: func(cmd *cobra.Command, args []string) {
 			if len(args) == 0 || (args[0] == "all") {
-				err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAll)
+				err := checkLoginAndRunWithConfig(cmd, cliConf, args, listAll)
 				if err != nil {
 					os.Exit(1)
 				}
@@ -39,7 +39,7 @@ func registerCommand_List(cliConf config.CLIConfig) *cobra.Command {
 		Aliases: []string{"applications", "app", "application"},
 		Short:   "Lists applications in a specific namespace, or across all namespaces",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listApps)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listApps)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -51,7 +51,7 @@ func registerCommand_List(cliConf config.CLIConfig) *cobra.Command {
 		Aliases: []string{"job"},
 		Short:   "Lists jobs in a specific namespace, or across all namespaces",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listJobs)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listJobs)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -63,7 +63,7 @@ func registerCommand_List(cliConf config.CLIConfig) *cobra.Command {
 		Aliases: []string{"addon"},
 		Short:   "Lists addons in a specific namespace, or across all namespaces",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listAddons)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listAddons)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -91,21 +91,16 @@ func registerCommand_List(cliConf config.CLIConfig) *cobra.Command {
 	return listCmd
 }
 
-func listAll(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.ListAll(ctx)
+func listAll(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.ListAll(ctx)
 		if err != nil {
 			return err
 		}
 		return nil
 	}
 
-	err = writeReleases(ctx, client, cliConf, "all")
+	err := writeReleases(ctx, client, cliConf, "all")
 	if err != nil {
 		return err
 	}
@@ -113,21 +108,16 @@ func listAll(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client
 	return nil
 }
 
-func listApps(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.ListApps(ctx)
+func listApps(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.ListApps(ctx)
 		if err != nil {
 			return err
 		}
 		return nil
 	}
 
-	err = writeReleases(ctx, client, cliConf, "application")
+	err := writeReleases(ctx, client, cliConf, "application")
 	if err != nil {
 		return err
 	}
@@ -135,21 +125,16 @@ func listApps(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client
 	return nil
 }
 
-func listJobs(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.ListJobs(ctx)
+func listJobs(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.ListJobs(ctx)
 		if err != nil {
 			return err
 		}
 		return nil
 	}
 
-	err = writeReleases(ctx, client, cliConf, "job")
+	err := writeReleases(ctx, client, cliConf, "job")
 	if err != nil {
 		return err
 	}
@@ -157,7 +142,7 @@ func listJobs(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client
 	return nil
 }
 
-func listAddons(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listAddons(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	err := writeReleases(ctx, client, cliConf, "addon")
 	if err != nil {
 		return err

+ 2 - 2
cli/cmd/commands/logs.go

@@ -20,7 +20,7 @@ func registerCommand_Logs(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Logs the output from a given application.",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, logs)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, logs)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -44,7 +44,7 @@ func registerCommand_Logs(cliConf config.CLIConfig) *cobra.Command {
 	return logsCmd
 }
 
-func logs(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
+func logs(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, args []string) error {
 	podsSimple, err := getPods(ctx, client, cliConfig, namespace, args[0])
 	if err != nil {
 		return fmt.Errorf("Could not retrieve list of pods: %s", err.Error())

+ 6 - 6
cli/cmd/commands/project.go

@@ -28,7 +28,7 @@ func registerCommand_Project(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Creates a project with the authorized user as admin",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, createProject)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, createProject)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -41,7 +41,7 @@ func registerCommand_Project(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Deletes the project with the given id",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteProject)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteProject)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -53,7 +53,7 @@ func registerCommand_Project(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "list",
 		Short: "Lists the projects for the logged in user",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listProjects)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listProjects)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -64,7 +64,7 @@ func registerCommand_Project(cliConf config.CLIConfig) *cobra.Command {
 	return projectCmd
 }
 
-func createProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func createProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	resp, err := client.CreateProject(ctx, &types.CreateProjectRequest{
 		Name: args[0],
 	})
@@ -77,7 +77,7 @@ func createProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, c
 	return cliConf.SetProject(ctx, client, resp.ID)
 }
 
-func listProjects(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listProjects(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	resp, err := client.ListUserProjects(ctx)
 	if err != nil {
 		return err
@@ -105,7 +105,7 @@ func listProjects(ctx context.Context, user *types.GetAuthenticatedUserResponse,
 	return nil
 }
 
-func deleteProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, args []string) error {
+func deleteProject(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, args []string) error {
 	userResp, err := utils.PromptPlaintext(
 		fmt.Sprintf(
 			`Are you sure you'd like to delete the project with id %s? %s `,

+ 8 - 8
cli/cmd/commands/registry.go

@@ -27,7 +27,7 @@ func registerCommand_Registry(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "list",
 		Short: "Lists the registries linked to a project",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listRegistries)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listRegistries)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -39,7 +39,7 @@ func registerCommand_Registry(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Deletes the registry with the given id",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, deleteRegistry)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, deleteRegistry)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -56,7 +56,7 @@ func registerCommand_Registry(cliConf config.CLIConfig) *cobra.Command {
 		Use:   "list",
 		Short: "Lists the repositories in an image registry",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listRepos)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listRepos)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -74,7 +74,7 @@ func registerCommand_Registry(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Lists the images the specified image repository",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, listImages)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, listImages)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -95,7 +95,7 @@ func registerCommand_Registry(cliConf config.CLIConfig) *cobra.Command {
 	return registryCmd
 }
 
-func listRegistries(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listRegistries(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	pID := cliConf.Project
 
 	// get the list of namespaces
@@ -129,7 +129,7 @@ func listRegistries(ctx context.Context, user *types.GetAuthenticatedUserRespons
 	return nil
 }
 
-func deleteRegistry(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func deleteRegistry(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	userResp, err := utils.PromptPlaintext(
 		fmt.Sprintf(
 			`Are you sure you'd like to delete the registry with id %s? %s `,
@@ -159,7 +159,7 @@ func deleteRegistry(ctx context.Context, user *types.GetAuthenticatedUserRespons
 	return nil
 }
 
-func listRepos(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listRepos(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	pID := cliConf.Project
 	rID := cliConf.Registry
 
@@ -189,7 +189,7 @@ func listRepos(ctx context.Context, user *types.GetAuthenticatedUserResponse, cl
 	return nil
 }
 
-func listImages(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func listImages(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	pID := cliConf.Project
 	rID := cliConf.Registry
 	repoName := args[0]

+ 4 - 4
cli/cmd/commands/run.go

@@ -48,7 +48,7 @@ func registerCommand_Run(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.MinimumNArgs(2),
 		Short: "Runs a command inside a connected cluster container.",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, run)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, run)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -61,7 +61,7 @@ func registerCommand_Run(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.NoArgs,
 		Short: "Delete any lingering ephemeral pods that were created with \"porter run\".",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, cleanup)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, cleanup)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -126,7 +126,7 @@ func registerCommand_Run(cliConf config.CLIConfig) *cobra.Command {
 	return runCmd
 }
 
-func run(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func run(ctx context.Context, user *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	execArgs := args[1:]
 
 	color.New(color.FgGreen).Println("Running", strings.Join(execArgs, " "), "for release", args[0])
@@ -242,7 +242,7 @@ func run(ctx context.Context, user *types.GetAuthenticatedUserResponse, client a
 	return executeRunEphemeral(ctx, config, namespace, selectedPod.Name, selectedContainerName, execArgs)
 }
 
-func cleanup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ []string) error {
+func cleanup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConfig config.CLIConfig, _ config.FeatureFlags, _ []string) error {
 	config := &PorterRunSharedConfig{
 		Client:    client,
 		CLIConfig: cliConfig,

+ 8 - 18
cli/cmd/commands/stack.go

@@ -37,7 +37,7 @@ func registerCommand_Stack(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Add an env group to a stack",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, stackAddEnvGroup)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, stackAddEnvGroup)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -49,7 +49,7 @@ func registerCommand_Stack(cliConf config.CLIConfig) *cobra.Command {
 		Args:  cobra.ExactArgs(1),
 		Short: "Remove an existing env group from a stack",
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, stackRemoveEnvGroup)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, stackRemoveEnvGroup)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -101,14 +101,9 @@ func registerCommand_Stack(cliConf config.CLIConfig) *cobra.Command {
 	return stackCmd
 }
 
-func stackAddEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.StackAddEnvGroup(ctx)
+func stackAddEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.StackAddEnvGroup(ctx)
 		if err != nil {
 			return err
 		}
@@ -184,14 +179,9 @@ func stackAddEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse
 	return nil
 }
 
-func stackRemoveEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.StackRemoveEnvGroup(ctx)
+func stackRemoveEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.StackRemoveEnvGroup(ctx)
 		if err != nil {
 			return err
 		}

+ 20 - 35
cli/cmd/commands/update.go

@@ -92,7 +92,7 @@ specify it as follows:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter update --app example-app --method docker --dockerfile ./docker/prod.Dockerfile"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateFull)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateFull)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -120,7 +120,7 @@ destination path for a .env file. For example:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter update get-env --app example-app --file .env"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateGetEnv)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateGetEnv)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -162,7 +162,7 @@ for the application:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter update build --app example-app --method docker --dockerfile ./prod.Dockerfile"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateBuild)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateBuild)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -202,7 +202,7 @@ linked it via "porter connect".
 			color.New(color.FgGreen, color.Bold).Sprintf("porter update push --app nginx --tag new-tag"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updatePush)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, updatePush)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -232,7 +232,7 @@ the image that the application uses if no --values file is specified:
 			color.New(color.FgGreen, color.Bold).Sprintf("porter update config --app example-app --tag custom-tag"),
 		),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateUpgrade)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateUpgrade)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -253,7 +253,7 @@ the image that the application uses if no --values file is specified:
 		Short: "Sets the desired value of an environment variable in an env group in the form VAR=VALUE.",
 		Args:  cobra.MaximumNArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateSetEnvGroup)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateSetEnvGroup)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -265,7 +265,7 @@ the image that the application uses if no --values file is specified:
 		Short: "Removes an environment variable from an env group.",
 		Args:  cobra.MinimumNArgs(1),
 		Run: func(cmd *cobra.Command, args []string) {
-			err := checkLoginAndRunWithConfig(cmd.Context(), cliConf, args, updateUnsetEnvGroup)
+			err := checkLoginAndRunWithConfig(cmd, cliConf, args, updateUnsetEnvGroup)
 			if err != nil {
 				os.Exit(1)
 			}
@@ -443,14 +443,9 @@ the image that the application uses if no --values file is specified:
 	return updateCmd
 }
 
-func updateFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.UpdateFull(ctx)
+func updateFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.UpdateFull(ctx)
 		if err != nil {
 			return err
 		}
@@ -508,7 +503,7 @@ func updateFull(ctx context.Context, _ *types.GetAuthenticatedUserResponse, clie
 	return nil
 }
 
-func updateGetEnv(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func updateGetEnv(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	updateAgent, err := updateGetAgent(ctx, client, cliConf)
 	if err != nil {
 		return err
@@ -531,14 +526,9 @@ func updateGetEnv(ctx context.Context, _ *types.GetAuthenticatedUserResponse, cl
 	return updateAgent.WriteBuildEnv(getEnvFileDest)
 }
 
-func updateBuild(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.UpdateBuild(ctx)
+func updateBuild(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.UpdateBuild(ctx)
 		if err != nil {
 			return err
 		}
@@ -553,7 +543,7 @@ func updateBuild(ctx context.Context, _ *types.GetAuthenticatedUserResponse, cli
 	return updateBuildWithAgent(ctx, updateAgent)
 }
 
-func updatePush(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func updatePush(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	if app == "" {
 		if len(args) == 0 {
 			return fmt.Errorf("please provide the docker image name")
@@ -612,14 +602,9 @@ func updatePush(ctx context.Context, _ *types.GetAuthenticatedUserResponse, clie
 	return updatePushWithAgent(ctx, updateAgent)
 }
 
-func updateUpgrade(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
-	project, err := client.GetProject(ctx, cliConf.Project)
-	if err != nil {
-		return fmt.Errorf("could not retrieve project from Porter API. Please contact support@porter.run")
-	}
-
-	if project.ValidateApplyV2 {
-		err = v2.UpdateUpgrade(ctx)
+func updateUpgrade(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
+	if featureFlags.ValidateApplyV2Enabled {
+		err := v2.UpdateUpgrade(ctx)
 		if err != nil {
 			return err
 		}
@@ -650,7 +635,7 @@ func updateUpgrade(ctx context.Context, _ *types.GetAuthenticatedUserResponse, c
 	return nil
 }
 
-func updateSetEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func updateSetEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	if len(normalEnvGroupVars) == 0 && len(secretEnvGroupVars) == 0 && len(args) == 0 {
 		return fmt.Errorf("please provide one or more variables to update")
 	}
@@ -758,7 +743,7 @@ func validateVarValue(in string) (string, string, error) {
 	return key, value, nil
 }
 
-func updateUnsetEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, args []string) error {
+func updateUnsetEnvGroup(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client api.Client, cliConf config.CLIConfig, featureFlags config.FeatureFlags, args []string) error {
 	if len(args) == 0 {
 		return fmt.Errorf("required variable name")
 	}

+ 12 - 31
cli/cmd/config/config.go

@@ -36,16 +36,19 @@ type CLIConfig struct {
 	Kubeconfig string `yaml:"kubeconfig"`
 }
 
+// FeatureFlags are any flags that are relevant to the feature set of the CLI. This should not include all feature flags, only those relevant to client-side CLI operations
+type FeatureFlags struct {
+	// ValidateApplyV2Enabled is a project-wide flag for checking if `porter apply` with porter.yaml is enabled
+	ValidateApplyV2Enabled bool
+}
+
 // InitAndLoadConfig populates the config object with the following precedence rules:
 // 1. flag
 // 2. env
 // 3. config
 // 4. default
+// Make sure to call overrideConfigWithFlags during runtime, to ensure that the flag values are considered
 func InitAndLoadConfig() (CLIConfig, error) {
-	return initAndLoadConfig()
-}
-
-func initAndLoadConfig() (CLIConfig, error) {
 	var config CLIConfig
 
 	porterDir, err := getOrCreatePorterDirectoryAndConfig()
@@ -56,6 +59,11 @@ func initAndLoadConfig() (CLIConfig, error) {
 	viper.SetConfigType("yaml")
 	viper.AddConfigPath(porterDir)
 
+	err = createAndLoadPorterYaml(porterDir)
+	if err != nil {
+		return config, fmt.Errorf("unable to load porter config: %w", err)
+	}
+
 	utils.DriverFlagSet.StringVar(
 		&config.Driver,
 		"driver",
@@ -141,11 +149,6 @@ func initAndLoadConfig() (CLIConfig, error) {
 		return config, err
 	}
 
-	err = createAndLoadPorterYaml(porterDir)
-	if err != nil {
-		return config, fmt.Errorf("unable to load porter config: %w", err)
-	}
-
 	err = viper.Unmarshal(&config)
 	if err != nil {
 		return config, fmt.Errorf("unable to unmarshal porter config: %w", err)
@@ -188,28 +191,6 @@ func createAndLoadPorterYaml(porterDir string) error {
 	return nil
 }
 
-// func GetCLIConfig() *CLIConfig {
-// 	if config == nil {
-// 		panic("GetCLIConfig() called before initialisation")
-// 	}
-
-// 	return config
-// }
-
-// func GetAPIClient() api.Client {
-// 	ctx := ctx
-
-// 	config := GetCLIConfig()
-
-// 	client := api.NewClientWithConfig(ctx, api.NewClientInput{
-// 		BaseURL:        fmt.Sprintf("%s/api", config.Host),
-// 		BearerToken:    config.Token,
-// 		CookieFileName: "cookie.json",
-// 	})
-
-// 	return client
-// }
-
 func (c *CLIConfig) SetDriver(driver string) error {
 	viper.Set("driver", driver)
 	color.New(color.FgGreen).Printf("Set the current driver as %s\n", driver)