Browse Source

Merge pull request #2636 from porter-dev/nafees/porter-yaml-v2beta1

[POR-880] porter.yaml v2beta1
Mohammed Nafees 3 năm trước cách đây
mục cha
commit
ecfa6b872a

+ 8 - 8
api/server/handlers/namespace/clone_env_group.go

@@ -50,12 +50,12 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	cm, _, err := agent.GetLatestVersionedConfigMap(request.Name, namespace)
+	cm, _, err := agent.GetLatestVersionedConfigMap(request.SourceName, namespace)
 
 	if err != nil {
 		if errors.Is(err, kubernetes.IsNotFoundError) {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", request.Name, namespace), http.StatusNotFound,
+				fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", request.SourceName, namespace), http.StatusNotFound,
 				"no config map found for envgroup",
 			))
 			return
@@ -65,12 +65,12 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	secret, _, err := agent.GetLatestVersionedSecret(request.Name, namespace)
+	secret, _, err := agent.GetLatestVersionedSecret(request.SourceName, namespace)
 
 	if err != nil {
 		if errors.Is(err, kubernetes.IsNotFoundError) {
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
-				fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", request.Name, namespace), http.StatusNotFound,
+				fmt.Errorf("error cloning env group: envgroup %s in namespace %s not found", request.SourceName, namespace), http.StatusNotFound,
 				"no k8s secret found for envgroup",
 			))
 			return
@@ -80,8 +80,8 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
-	if request.CloneName == "" {
-		request.CloneName = request.Name
+	if request.TargetName == "" {
+		request.TargetName = request.SourceName
 	}
 
 	vars := make(map[string]string)
@@ -98,8 +98,8 @@ func (c *CloneEnvGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	}
 
 	configMap, err := envgroup.CreateEnvGroup(agent, types.ConfigMapInput{
-		Name:            request.CloneName,
-		Namespace:       request.Namespace,
+		Name:            request.TargetName,
+		Namespace:       request.TargetNamespace,
 		Variables:       vars,
 		SecretVariables: secretVars,
 	})

+ 4 - 4
api/types/namespace.go

@@ -133,10 +133,10 @@ type GetEnvGroupRequest struct {
 }
 
 type CloneEnvGroupRequest struct {
-	Namespace string `json:"namespace" form:"required"`
-	Name      string `json:"name" form:"required,dns1123"`
-	CloneName string `json:"clone_name,dns1123"`
-	Version   uint   `json:"version"`
+	TargetNamespace string `json:"namespace" form:"required"`
+	SourceName      string `json:"name" form:"required,dns1123"`
+	TargetName      string `json:"clone_name,dns1123"`
+	Version         uint   `json:"version"`
 }
 
 type GetEnvGroupAllRequest struct {

+ 77 - 12
cli/cmd/apply.go

@@ -20,6 +20,7 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/deploy/wait"
 	"github.com/porter-dev/porter/cli/cmd/preview"
+	previewV2Beta1 "github.com/porter-dev/porter/cli/cmd/preview/v2beta1"
 	previewInt "github.com/porter-dev/porter/internal/integrations/preview"
 	"github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/porter-dev/switchboard/pkg/drivers"
@@ -29,6 +30,7 @@ import (
 	switchboardWorker "github.com/porter-dev/switchboard/pkg/worker"
 	"github.com/rs/zerolog"
 	"github.com/spf13/cobra"
+	"gopkg.in/yaml.v2"
 )
 
 // applyCmd represents the "porter apply" base command when called
@@ -105,24 +107,54 @@ func init() {
 }
 
 func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string) error {
-	if _, ok := os.LookupEnv("PORTER_VALIDATE_YAML"); ok {
-		err := applyValidate()
-
-		if err != nil {
-			return err
-		}
-	}
-
 	fileBytes, err := ioutil.ReadFile(porterYAML)
 
 	if err != nil {
 		return fmt.Errorf("error reading porter.yaml: %w", err)
 	}
 
-	resGroup, err := parser.ParseRawBytes(fileBytes)
+	var previewVersion struct {
+		Version string `json:"version"`
+	}
+
+	err = yaml.Unmarshal(fileBytes, &previewVersion)
 
 	if err != nil {
-		return fmt.Errorf("error parsing porter.yaml: %w", err)
+		return fmt.Errorf("error unmarshaling porter.yaml: %w", err)
+	}
+
+	var resGroup *switchboardTypes.ResourceGroup
+
+	if previewVersion.Version == "v2beta1" {
+		ns := os.Getenv("PORTER_NAMESPACE")
+
+		applier, err := previewV2Beta1.NewApplier(client, fileBytes, ns)
+
+		if err != nil {
+			return err
+		}
+
+		resGroup, err = applier.DowngradeToV1()
+
+		if err != nil {
+			return err
+		}
+	} else if previewVersion.Version == "v1" {
+		if _, ok := os.LookupEnv("PORTER_VALIDATE_YAML"); ok {
+			err := applyValidate()
+
+			if err != nil {
+				return err
+			}
+		}
+
+		resGroup, err = parser.ParseRawBytes(fileBytes)
+
+		if err != nil {
+			return fmt.Errorf("error parsing porter.yaml: %w", err)
+		}
+	} else {
+		return fmt.Errorf("unknown porter.yaml version: %s", previewVersion.Version)
 	}
 
 	basePath, err := os.Getwd()
@@ -158,6 +190,9 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 		worker.RegisterHook("deployment", deploymentHook)
 	}
 
+	errorEmitterHook := NewErrorEmitterHook(client, resGroup)
+	worker.RegisterHook("erroremitter", errorEmitterHook)
+
 	cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
 	worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
 
@@ -1173,8 +1208,8 @@ func (t *CloneEnvGroupHook) PreApply() error {
 					_, err = t.client.CloneEnvGroup(
 						context.Background(), target.Project, target.Cluster, group.Namespace,
 						&types.CloneEnvGroupRequest{
-							Name:      group.Name,
-							Namespace: target.Namespace,
+							SourceName:      group.Name,
+							TargetNamespace: target.Namespace,
 						},
 					)
 
@@ -1234,3 +1269,33 @@ func isSystemNamespace(namespace string) bool {
 		namespace == "porter-agent-system" || namespace == "default" ||
 		namespace == "ingress-nginx-private"
 }
+
+type ErrorEmitterHook struct{}
+
+func NewErrorEmitterHook(*api.Client, *switchboardTypes.ResourceGroup) *ErrorEmitterHook {
+	return &ErrorEmitterHook{}
+}
+
+func (t *ErrorEmitterHook) PreApply() error {
+	return nil
+}
+
+func (t *ErrorEmitterHook) DataQueries() map[string]interface{} {
+	return nil
+}
+
+func (t *ErrorEmitterHook) PostApply(map[string]interface{}) error {
+	return nil
+}
+
+func (t *ErrorEmitterHook) OnError(err error) {
+	color.New(color.FgRed).Fprintf(os.Stderr, "Errors while building: %s\n", err.Error())
+}
+
+func (t *ErrorEmitterHook) OnConsolidatedErrors(errMap map[string]error) {
+	color.New(color.FgRed).Fprintf(os.Stderr, "Errors while building:\n")
+
+	for resName, err := range errMap {
+		color.New(color.FgRed).Fprintf(os.Stderr, "  - %s: %s\n", resName, err.Error())
+	}
+}

+ 42 - 0
cli/cmd/preview/v2beta1/addon_resource.go

@@ -0,0 +1,42 @@
+package v2beta1
+
+import "github.com/porter-dev/switchboard/pkg/types"
+
+func (a *AddonResource) GetName() string {
+	if a == nil || a.Name == nil {
+		return ""
+	}
+
+	return *a.Name
+}
+
+func (a *AddonResource) GetDependsOn() []string {
+	var dependsOn []string
+
+	if a == nil || a.DependsOn == nil {
+		return dependsOn
+	}
+
+	for _, d := range a.DependsOn {
+		if d == nil {
+			continue
+		}
+
+		dependsOn = append(dependsOn, *d)
+	}
+
+	return dependsOn
+}
+
+func (a *AddonResource) getV1Addon() (*types.Resource, error) {
+	return &types.Resource{
+		Name: a.GetName(),
+		Source: map[string]interface{}{
+			"name":    a.Chart.GetName(),
+			"repo":    a.Chart.GetURL(),
+			"version": a.Chart.GetVersion(),
+		},
+		DependsOn: a.GetDependsOn(),
+		Config:    a.HelmValues,
+	}, nil
+}

+ 82 - 0
cli/cmd/preview/v2beta1/app_resource.go

@@ -0,0 +1,82 @@
+package v2beta1
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/mitchellh/mapstructure"
+	apiTypes "github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/integrations/preview"
+	"github.com/porter-dev/switchboard/pkg/types"
+)
+
+func (a *AppResource) GetName() string {
+	if a == nil || a.Name == nil {
+		return ""
+	}
+
+	return *a.Name
+}
+
+func (a *AppResource) GetDependsOn() []string {
+	var dependsOn []string
+
+	if a == nil || a.DependsOn == nil {
+		return dependsOn
+	}
+
+	for _, d := range a.DependsOn {
+		if d == nil {
+			continue
+		}
+
+		dependsOn = append(dependsOn, *d)
+	}
+
+	return dependsOn
+}
+
+func (a *AppResource) GetBuildRef() string {
+	if a == nil || a.BuildRef == nil {
+		return ""
+	}
+
+	return *a.BuildRef
+}
+
+func (a *AppResource) getV1Resource(b *Build) (*types.Resource, error) {
+	config := &preview.ApplicationConfig{}
+
+	config.Build.Method = "registry"
+	config.Build.Image = fmt.Sprintf("\"{ .%s.image }\"", b.GetName())
+	config.Build.Env = b.GetRawEnv()
+	config.Values = a.HelmValues
+
+	for _, eg := range b.GetEnvGroups() {
+		ns, name, _ := strings.Cut(eg, "/")
+
+		config.EnvGroups = append(config.EnvGroups, apiTypes.EnvGroupMeta{
+			Name:      name,
+			Namespace: ns,
+		})
+	}
+
+	rawConfig := make(map[string]any)
+
+	err := mapstructure.Decode(config, &rawConfig)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &types.Resource{
+		Name:      a.GetName(),
+		DependsOn: append([]string{b.GetName()}, a.GetDependsOn()...),
+		Source: map[string]any{
+			"name":    a.Chart.GetName(),
+			"repo":    a.Chart.GetURL(),
+			"version": a.Chart.GetVersion(),
+		},
+		Config: rawConfig,
+	}, nil
+}

+ 414 - 0
cli/cmd/preview/v2beta1/apply.go

@@ -0,0 +1,414 @@
+package v2beta1
+
+import (
+	"context"
+	"fmt"
+
+	api "github.com/porter-dev/porter/api/client"
+	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/switchboard/pkg/types"
+	"gopkg.in/yaml.v3"
+)
+
+// const (
+// 	constantsEnvGroup = "preview-env-constants"
+
+// 	defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~`!@#$%^&*()_+-={}[]"
+// )
+
+type PreviewApplier struct {
+	apiClient *api.Client
+	rawBytes  []byte
+	namespace string
+	parsed    *PorterYAML
+
+	// variablesMap map[string]string
+	// osEnv        map[string]string
+	// envGroups    map[string]*apiTypes.EnvGroup
+}
+
+func NewApplier(client *api.Client, raw []byte, namespace string) (*PreviewApplier, error) {
+	parsed := &PorterYAML{}
+
+	err := yaml.Unmarshal(raw, parsed)
+
+	if err != nil {
+		errMsg := composePreviewMessage("error parsing porter.yaml", Error)
+		return nil, fmt.Errorf("%s: %w", errMsg, err)
+	}
+
+	// err = validator.ValidatePorterYAML(parsed)
+
+	// if err != nil {
+	// 	return nil, err
+	// }
+
+	err = validateCLIEnvironment(namespace)
+
+	if err != nil {
+		errMsg := composePreviewMessage("porter CLI is not configured correctly", Error)
+		return nil, fmt.Errorf("%s: %w", errMsg, err)
+	}
+
+	return &PreviewApplier{
+		apiClient: client,
+		rawBytes:  raw,
+		namespace: namespace,
+		parsed:    parsed,
+	}, nil
+}
+
+func validateCLIEnvironment(namespace string) error {
+	if config.GetCLIConfig().Token == "" {
+		return fmt.Errorf("no auth token present, please run 'porter auth login' to authenticate")
+	}
+
+	if config.GetCLIConfig().Project == 0 {
+		return fmt.Errorf("no project selected, please run 'porter config set-project' to select a project")
+	}
+
+	if config.GetCLIConfig().Cluster == 0 {
+		return fmt.Errorf("no cluster selected, please run 'porter config set-cluster' to select a cluster")
+	}
+
+	// if namespace == "" {
+	// 	printInfoMessage("no namespace provided, falling back to namespace 'default'")
+	// }
+
+	return nil
+}
+
+func (a *PreviewApplier) Apply() error {
+	// for v2beta1, check if the namespace exists in the current project-cluster pair
+	//
+	// this is a sanity check to ensure that the user does not see any internal
+	// errors that are caused by the namespace not existing
+	nsList, err := a.apiClient.GetK8sNamespaces(
+		context.Background(),
+		config.GetCLIConfig().Project,
+		config.GetCLIConfig().Cluster,
+	)
+
+	if err != nil {
+		errMsg := composePreviewMessage(fmt.Sprintf("error listing namespaces for project '%d', cluster '%d'",
+			config.GetCLIConfig().Project, config.GetCLIConfig().Cluster), Error)
+		return fmt.Errorf("%s: %w", errMsg, err)
+	}
+
+	namespaces := *nsList
+	nsFound := false
+
+	for _, ns := range namespaces {
+		if ns.Name == a.namespace {
+			nsFound = true
+			break
+		}
+	}
+
+	if !nsFound {
+		// 	errMsg := composePreviewMessage(fmt.Sprintf("namespace '%s' does not exist in project '%d', cluster '%d'",
+		// 		a.namespace, config.GetCLIConfig().Project, config.GetCLIConfig().Cluster), Error)
+		// 	return fmt.Errorf("%s: %w", errMsg, err)
+	}
+
+	printInfoMessage(fmt.Sprintf("Applying porter.yaml with the following attributes:\n"+
+		"\tHost: %s\n\tProject ID: %d\n\tCluster ID: %d\n\tNamespace: %s",
+		config.GetCLIConfig().Host,
+		config.GetCLIConfig().Project,
+		config.GetCLIConfig().Cluster,
+		a.namespace),
+	)
+
+	// err = a.readOSEnv()
+
+	// if err != nil {
+	// 	errMsg := composePreviewMessage("error reading OS environment variables", Error)
+	// 	return fmt.Errorf("%s: %w", errMsg, err)
+	// }
+
+	// err = a.processVariables()
+
+	// if err != nil {
+	// 	return err
+	// }
+
+	// err = a.processEnvGroups()
+
+	// if err != nil {
+	// 	return err
+	// }
+
+	return nil
+}
+
+func (a *PreviewApplier) DowngradeToV1() (*types.ResourceGroup, error) {
+	err := a.Apply()
+
+	if err != nil {
+		return nil, err
+	}
+
+	v1File := &types.ResourceGroup{
+		Version: "v1",
+	}
+
+	buildRefs := make(map[string]*Build)
+
+	for _, b := range a.parsed.Builds {
+		if b == nil {
+			continue
+		}
+
+		buildRefs[b.GetName()] = b
+
+		bi, err := b.getV1BuildImage()
+
+		if err != nil {
+			return nil, err
+		}
+
+		pi, err := b.getV1PushImage()
+
+		if err != nil {
+			return nil, err
+		}
+
+		v1File.Resources = append(v1File.Resources, bi, pi)
+	}
+
+	for _, app := range a.parsed.Apps {
+		if app == nil {
+			continue
+		}
+
+		if _, ok := buildRefs[app.GetBuildRef()]; !ok {
+			errMsg := composePreviewMessage(fmt.Sprintf("build_ref '%s' referenced by app '%s' does not exist",
+				app.GetBuildRef(), app.GetName()), Error)
+			return nil, fmt.Errorf("%s: %w", errMsg, err)
+		}
+
+		ai, err := app.getV1Resource(buildRefs[app.GetBuildRef()])
+
+		if err != nil {
+			return nil, err
+		}
+
+		v1File.Resources = append(v1File.Resources, ai)
+	}
+
+	for _, addon := range a.parsed.Addons {
+		if addon == nil {
+			continue
+		}
+
+		ai, err := addon.getV1Addon()
+
+		if err != nil {
+			return nil, err
+		}
+
+		v1File.Resources = append(v1File.Resources, ai)
+	}
+
+	return v1File, nil
+}
+
+// func (a *PreviewApplier) readOSEnv() error {
+// 	printInfoMessage("Reading OS environment variables")
+
+// 	env := os.Environ()
+// 	osEnv := make(map[string]string)
+
+// 	for _, e := range env {
+// 		k, v, _ := strings.Cut(e, "=")
+// 		kCopy := k
+
+// 		if k != "" && v != "" && strings.HasPrefix(k, "PORTER_APPLY_") {
+// 			// we only read in env variables that start with PORTER_APPLY_
+// 			for strings.HasPrefix(k, "PORTER_APPLY_") {
+// 				k = strings.TrimPrefix(k, "PORTER_APPLY_")
+// 			}
+
+// 			if k == "" {
+// 				printWarningMessage(fmt.Sprintf("Ignoring invalid OS environment variable '%s'", kCopy))
+// 			}
+
+// 			osEnv[k] = v
+// 		}
+// 	}
+
+// 	a.osEnv = osEnv
+
+// 	return nil
+// }
+
+// func (a *PreviewApplier) processVariables() error {
+// 	printInfoMessage("Processing variables")
+
+// 	constantsMap := make(map[string]string)
+// 	variablesMap := make(map[string]string)
+
+// 	for _, v := range a.parsed.Variables {
+// 		if v == nil {
+// 			continue
+// 		}
+
+// 		if v.Once != nil && *v.Once {
+// 			// a constant which should be stored in the env group on first run
+// 			if exists, err := a.constantExistsInEnvGroup(*v.Name); err == nil {
+// 				if exists == nil {
+// 					// this should not happen
+// 					return fmt.Errorf("internal error: please let the Porter team know about this and quote the following " +
+// 						"error:\n-----\nERROR: checking for constant existence in env group returned nil with no error")
+// 				}
+
+// 				val := *exists
+
+// 				if !val {
+// 					// create the constant in the env group
+// 					if *v.Value != "" {
+// 						constantsMap[*v.Name] = *v.Value
+// 					} else if v.Random != nil && *v.Random {
+// 						constantsMap[*v.Name] = randomString(*v.Length, defaultCharset)
+// 					} else {
+// 						// this should not happen
+// 						return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
+// 							"error:\n-----\nERROR: for variable '%s', random is false and value is empty", *v.Name)
+// 					}
+// 				}
+// 			} else {
+// 				return fmt.Errorf("error checking for existence of constant %s: %w", *v.Name, err)
+// 			}
+// 		} else {
+// 			if v.Value != nil && *v.Value != "" {
+// 				variablesMap[*v.Name] = *v.Value
+// 			} else if v.Random != nil && *v.Random {
+// 				variablesMap[*v.Name] = randomString(*v.Length, defaultCharset)
+// 			} else {
+// 				// this should not happen
+// 				return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
+// 					"error:\n-----\nERROR: for variable '%s', random is false and value is empty", *v.Name)
+// 			}
+// 		}
+// 	}
+
+// 	if len(constantsMap) > 0 {
+// 		// we need to create these constants in the env group
+// 		_, err := a.apiClient.CreateEnvGroup(
+// 			context.Background(),
+// 			config.GetCLIConfig().Project,
+// 			config.GetCLIConfig().Cluster,
+// 			a.namespace,
+// 			&apiTypes.CreateEnvGroupRequest{
+// 				Name:      constantsEnvGroup,
+// 				Variables: constantsMap,
+// 			},
+// 		)
+
+// 		if err != nil {
+// 			return fmt.Errorf("error creating constants (variables with once set to true) in env group: %w", err)
+// 		}
+
+// 		for k, v := range constantsMap {
+// 			variablesMap[k] = v
+// 		}
+// 	}
+
+// 	a.variablesMap = variablesMap
+
+// 	return nil
+// }
+
+// func (a *PreviewApplier) constantExistsInEnvGroup(name string) (*bool, error) {
+// 	apiResponse, err := a.apiClient.GetEnvGroup(
+// 		context.Background(),
+// 		config.GetCLIConfig().Project,
+// 		config.GetCLIConfig().Cluster,
+// 		a.namespace,
+// 		&apiTypes.GetEnvGroupRequest{
+// 			Name: constantsEnvGroup,
+// 			// we do not care about the version because it always needs to be the latest
+// 		},
+// 	)
+
+// 	if err != nil {
+// 		if strings.Contains(err.Error(), "env group not found") {
+// 			return booleanptr(false), nil
+// 		}
+
+// 		return nil, err
+// 	}
+
+// 	if _, ok := apiResponse.Variables[name]; ok {
+// 		return booleanptr(true), nil
+// 	}
+
+// 	return booleanptr(false), nil
+// }
+
+// func (a *PreviewApplier) processEnvGroups() error {
+// 	printInfoMessage("Processing env groups")
+
+// 	for _, eg := range a.parsed.EnvGroups {
+// 		if eg == nil {
+// 			continue
+// 		}
+
+// 		if eg.Name == nil || *eg.Name == "" {
+
+// 		}
+
+// 		envGroup, err := a.apiClient.GetEnvGroup(
+// 			context.Background(),
+// 			config.GetCLIConfig().Project,
+// 			config.GetCLIConfig().Cluster,
+// 			a.namespace,
+// 			&apiTypes.GetEnvGroupRequest{
+// 				Name: *eg.Name,
+// 			},
+// 		)
+
+// 		if err != nil && strings.Contains(err.Error(), "env group not found") {
+// 			if eg.CloneFrom == nil {
+// 				return fmt.Errorf(composePreviewMessage(fmt.Sprintf("empty clone_from for env group '%s'", *eg.Name), Error))
+// 			}
+
+// 			egNS, egName, found := strings.Cut(*eg.CloneFrom, "/")
+
+// 			if !found {
+// 				return fmt.Errorf("error parsing clone_from for env group '%s': invalid format", *eg.Name)
+// 			}
+
+// 			// clone the env group
+// 			envGroup, err := a.apiClient.CloneEnvGroup(
+// 				context.Background(),
+// 				config.GetCLIConfig().Project,
+// 				config.GetCLIConfig().Cluster,
+// 				egNS,
+// 				&apiTypes.CloneEnvGroupRequest{
+// 					SourceName:      egName,
+// 					TargetNamespace: a.namespace,
+// 					TargetName:      *eg.Name,
+// 				},
+// 			)
+
+// 			if err != nil {
+// 				return fmt.Errorf("error cloning env group '%s' from '%s': %w", egName, egNS, err)
+// 			}
+
+// 			a.envGroups[*eg.Name] = &apiTypes.EnvGroup{
+// 				Name:      envGroup.Name,
+// 				Variables: envGroup.Variables,
+// 			}
+// 		} else if err != nil {
+// 			return fmt.Errorf("error checking for env group '%s': %w", *eg.Name, err)
+// 		} else {
+// 			a.envGroups[*eg.Name] = &apiTypes.EnvGroup{
+// 				Name:      envGroup.Name,
+// 				Variables: envGroup.Variables,
+// 			}
+// 		}
+// 	}
+
+// 	return nil
+// }

+ 196 - 0
cli/cmd/preview/v2beta1/build.go

@@ -0,0 +1,196 @@
+package v2beta1
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/mitchellh/mapstructure"
+	apiTypes "github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/integrations/preview"
+	"github.com/porter-dev/switchboard/pkg/types"
+)
+
+func (b *Build) GetName() string {
+	if b == nil || b.Name == nil {
+		return ""
+	}
+
+	return *b.Name
+}
+
+func (b *Build) GetContext() string {
+	if b == nil || b.Context == nil || *b.Context == "" {
+		return "."
+	}
+
+	return *b.Context
+}
+
+func (b *Build) GetMethod() string {
+	if b == nil || b.Method == nil {
+		return ""
+	}
+
+	return *b.Method
+}
+
+func (b *Build) GetBuilder() string {
+	if b == nil || b.Builder == nil {
+		return ""
+	}
+
+	return *b.Builder
+}
+
+func (b *Build) GetBuildpacks() []string {
+	if b == nil || b.Buildpacks == nil {
+		return []string{}
+	}
+
+	var bp []string
+
+	for _, b := range b.Buildpacks {
+		if b == nil {
+			continue
+		}
+
+		bp = append(bp, *b)
+	}
+
+	return bp
+}
+
+func (b *Build) GetDockerfile() string {
+	if b == nil || b.Dockerfile == nil {
+		return ""
+	}
+
+	return *b.Dockerfile
+}
+
+func (b *Build) GetImage() string {
+	if b == nil || b.Image == nil {
+		return ""
+	}
+
+	return *b.Image
+}
+
+func (b *Build) GetRawEnv() map[string]string {
+	env := make(map[string]string)
+
+	if b == nil || b.Env == nil {
+		return env
+	}
+
+	for k, v := range b.Env.Raw {
+		if k == nil || v == nil {
+			continue
+		}
+
+		env[*k] = *v
+	}
+
+	return env
+}
+
+func (b *Build) GetEnvGroups() []string {
+	var eg []string
+
+	if b == nil || b.Env == nil {
+		return eg
+	}
+
+	for _, g := range b.Env.ImportFrom {
+		if g == nil {
+			continue
+		}
+
+		ns, name, valid := strings.Cut(*g, "/")
+
+		if !valid || ns == "" || name == "" {
+			printWarningMessage(fmt.Sprintf("ignoring invalid env group name: %s", *g))
+			continue
+		}
+
+		eg = append(eg, *g)
+	}
+
+	return eg
+}
+
+func (b *Build) getV1BuildImage() (*types.Resource, error) {
+	config := &preview.BuildDriverConfig{}
+
+	if b.GetMethod() == "pack" {
+		config.Build.Method = "pack"
+		config.Build.Builder = b.GetBuilder()
+		config.Build.Buildpacks = b.GetBuildpacks()
+	} else if b.GetMethod() == "docker" {
+		config.Build.Method = "docker"
+		config.Build.Dockerfile = b.GetDockerfile()
+	} else if b.GetMethod() == "registry" {
+		config.Build.Method = "registry"
+		config.Build.Image = b.GetImage()
+	} else {
+		return nil, fmt.Errorf("invalid build method: %s", b.GetMethod())
+	}
+
+	config.Build.Context = b.GetContext()
+	config.Build.Env = b.GetRawEnv()
+
+	for _, eg := range b.GetEnvGroups() {
+		ns, name, _ := strings.Cut(eg, "/")
+
+		config.EnvGroups = append(config.EnvGroups, apiTypes.EnvGroupMeta{
+			Namespace: ns,
+			Name:      name,
+		})
+	}
+
+	rawConfig := make(map[string]any)
+
+	err := mapstructure.Decode(config, &rawConfig)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &types.Resource{
+		Name:   fmt.Sprintf("%s-build-image", b.GetName()),
+		Driver: "build-image",
+		Source: map[string]any{
+			"name": "web",
+		},
+		Target: map[string]any{
+			"app_name": b.GetName(),
+		},
+		Config: rawConfig,
+	}, nil
+}
+
+func (b *Build) getV1PushImage() (*types.Resource, error) {
+	config := &preview.PushDriverConfig{}
+
+	config.Push.Image = fmt.Sprintf("\"{ .%s-build-image.image }\"", b.GetName())
+
+	rawConfig := make(map[string]any)
+
+	err := mapstructure.Decode(config, &rawConfig)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &types.Resource{
+		Name:   b.GetName(),
+		Driver: "push-image",
+		DependsOn: []string{
+			fmt.Sprintf("%s-build-image", b.GetName()),
+		},
+		Target: map[string]any{
+			"app_name": b.GetName(),
+		},
+		Config: rawConfig,
+	}, nil
+}

+ 375 - 0
cli/cmd/preview/v2beta1/default_driver.go

@@ -0,0 +1,375 @@
+package v2beta1
+
+// import (
+// 	"context"
+// 	"fmt"
+// 	"os"
+// 	"strings"
+
+// 	"github.com/cli/cli/git"
+// 	"github.com/fatih/color"
+// 	"github.com/mitchellh/mapstructure"
+// 	api "github.com/porter-dev/porter/api/client"
+// 	apiTypes "github.com/porter-dev/porter/api/types"
+// 	"github.com/porter-dev/porter/cli/cmd/config"
+// 	"github.com/porter-dev/porter/cli/cmd/deploy"
+// 	"github.com/porter-dev/switchboard/v2/pkg/types"
+// )
+
+// type DefaultDriver struct {
+// 	Vars      map[string]string
+// 	Env       map[string]string
+// 	Builds    []*types.Build
+// 	APIClient *api.Client
+// 	Namespace string
+
+// 	allErrors []error
+// }
+
+// func (d *DefaultDriver) PreApply(resource *types.YAMLNode[*types.Resource]) error {
+// 	return nil
+// }
+
+// func (d *DefaultDriver) Apply(resource *types.YAMLNode[*types.Resource]) error {
+// 	if isPorterApp(resource) {
+// 		return d.applyPorterApp(resource)
+// 	}
+
+// 	// everything else is an addon
+// 	return d.applyAddon(resource)
+// }
+
+// func (d *DefaultDriver) PostApply(resource *types.YAMLNode[*types.Resource]) error {
+// 	return nil
+// }
+
+// func (d *DefaultDriver) OnError(resource *types.YAMLNode[*types.Resource], errs []error) {
+
+// }
+
+// func isPorterApp(resource *types.YAMLNode[*types.Resource]) bool {
+// 	if resource.GetValue().ChartURL.GetValue() == "https://charts.getporter.dev" &&
+// 		(resource.GetValue().Type.GetValue() == "web" ||
+// 			resource.GetValue().Type.GetValue() == "worker" ||
+// 			resource.GetValue().Type.GetValue() == "job") {
+// 		return true
+// 	}
+
+// 	return false
+// }
+
+// func (d *DefaultDriver) applyPorterApp(resource *types.YAMLNode[*types.Resource]) error {
+// 	appBuild := &porterAppBuild{}
+// 	appDeploy := &porterAppDeploy{}
+// 	buildNode := resource.GetValue().Build.GetRawYAMLNode()
+// 	deployNode := resource.GetValue().Deploy.GetRawYAMLNode()
+
+// 	err := buildNode.Decode(appBuild)
+
+// 	if err != nil {
+// 		return err // FIXME: descriptive error
+// 	}
+
+// 	err = deployNode.Decode(appDeploy)
+
+// 	if err != nil {
+// 		return err // FIXME: descriptive error
+// 	}
+
+// 	var buildConfig *types.Build
+
+// 	if appBuild.Ref != "" {
+// 		for _, b := range d.Builds {
+// 			if b.Name.GetValue() == appBuild.Ref {
+// 				buildConfig = b
+// 				break
+// 			}
+// 		}
+
+// 		if buildConfig == nil {
+// 			// this should not happen
+// 			return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
+// 				"error:\n-----\nERROR: invalid build ref given for app '%s'", resource.GetValue().Name.GetValue())
+// 		}
+// 	} else {
+// 		buildConfig = appBuild.Build
+// 	}
+
+// 	if buildConfig == nil {
+// 		// this should not happen
+// 		return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
+// 			"error:\n-----\nERROR: neither build ref nor build body given for app '%s'", resource.GetValue().Name.GetValue())
+// 	}
+
+// 	if resource.GetValue().Type.GetValue() == "job" {
+// 		jobConfig := &porterJob{}
+// 		jobNode := resource.GetRawYAMLNode()
+
+// 		err := jobNode.Decode(jobConfig)
+
+// 		if err != nil {
+// 			return err // FIXME: descriptive error
+// 		}
+
+// 		return d.applyJob(resource, buildConfig, appDeploy, jobConfig)
+// 	} else if oneOf(resource.GetValue().Type.GetValue(), "web", "worker") {
+
+// 	} else {
+// 		// this should not happen
+// 		return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
+// 			"error:\n-----\nERROR: app '%s' is not one of 'web', 'worker', 'job'", resource.GetValue().Name.GetValue())
+// 	}
+
+// 	return nil
+// }
+
+// func (d *DefaultDriver) applyAddon(resource *types.YAMLNode[*types.Resource]) error {
+// 	return nil
+// }
+
+// func (d *DefaultDriver) applyJob(
+// 	resource *types.YAMLNode[*types.Resource],
+// 	buildConfig *types.Build,
+// 	appDeploy *porterAppDeploy,
+// 	jobConfig *porterJob,
+// ) error {
+// 	_, err := d.APIClient.GetRelease(
+// 		context.Background(),
+// 		config.GetCLIConfig().Project,
+// 		config.GetCLIConfig().Cluster,
+// 		d.Namespace,
+// 		resource.GetValue().Name.GetValue(),
+// 	)
+
+// 	exists := err == nil
+
+// 	flattenedBuildEnv := make(map[string]string)
+
+// 	for k, v := range buildConfig.Env {
+// 		flattenedBuildEnv[k.GetValue()] = v.GetValue()
+// 	}
+
+// 	var flattenedBuildEnvGroup []apiTypes.EnvGroupMeta
+
+// 	for _, egName := range buildConfig.EnvGroups {
+// 		flattenedBuildEnvGroup = append(flattenedBuildEnvGroup, apiTypes.EnvGroupMeta{
+// 			Name:      egName.GetValue(),
+// 			Namespace: d.Namespace,
+// 		})
+// 	}
+
+// 	tag := getImageTag()
+
+// 	sharedOpts := &deploy.SharedOpts{
+// 		ProjectID:       config.GetCLIConfig().Project,
+// 		ClusterID:       config.GetCLIConfig().Cluster,
+// 		Namespace:       d.Namespace,
+// 		LocalPath:       buildConfig.Context.GetValue(),
+// 		LocalDockerfile: buildConfig.Dockerfile.GetValue(),
+// 		OverrideTag:     tag,
+// 		Method:          deploy.DeployBuildType(buildConfig.Method.GetValue()),
+// 		AdditionalEnv:   flattenedBuildEnv,
+// 		EnvGroups:       flattenedBuildEnvGroup,
+// 	}
+
+// 	if buildConfig.Method.GetValue() == "pack" && buildConfig.UseCache != nil {
+// 		sharedOpts.UseCache = buildConfig.UseCache.GetValue()
+// 	}
+
+// 	if exists {
+// 		if jobConfig.Once {
+// 			// since the job already exists and was marked 'once', simply return
+// 			return nil
+// 		}
+
+// 		updateAgent, err := deploy.NewDeployAgent(d.APIClient, resource.GetValue().Name.GetValue(), &deploy.DeployOpts{
+// 			SharedOpts: sharedOpts,
+// 			Local:      buildConfig.Method.GetValue() != "registry",
+// 		})
+
+// 		if err != nil {
+// 			return fmt.Errorf("[porter.yaml v2][app:%s] error creating deploy agent to update app: %w",
+// 				resource.GetValue().Name.GetValue(), err)
+// 		}
+
+// 		// if the build method is registry, we do not trigger a build
+// 		if buildConfig.Method.GetValue() != "registry" {
+// 			buildEnv, err := updateAgent.GetBuildEnv(&deploy.GetBuildEnvOpts{
+// 				UseNewConfig: true,
+// 				// NewConfig:    appConf.Values,
+// 			})
+
+// 			if err != nil {
+// 				return err // FIXME
+// 			}
+
+// 			err = updateAgent.SetBuildEnv(buildEnv)
+
+// 			if err != nil {
+// 				return err // FIXME
+// 			}
+
+// 			var bc *apiTypes.BuildConfig
+
+// 			if buildConfig.Method.GetValue() == "pack" {
+// 				// FIXME: temporary fix
+// 				var bp []string
+
+// 				for _, b := range buildConfig.Buildpacks {
+// 					bp = append(bp, b.GetValue())
+// 				}
+
+// 				bc = &apiTypes.BuildConfig{
+// 					Builder:    buildConfig.Builder.GetValue(),
+// 					Buildpacks: bp,
+// 				}
+// 			}
+
+// 			err = updateAgent.Build(bc)
+
+// 			if err != nil {
+// 				return err // FIXME
+// 			}
+
+// 			// if !appConf.Build.UseCache { // FIXME
+// 			err = updateAgent.Push()
+
+// 			if err != nil {
+// 				return err // FIXME
+// 			}
+// 			// }
+// 		}
+
+// 		// err = updateAgent.UpdateImageAndValues(appConf.Values) // FIXME
+
+// 		// if err != nil {
+// 		// 	return err // FIXME
+// 		// }
+// 	} else { // create the job
+// 		// attempt to get repo suffix from environment variables
+// 		var repoSuffix string
+
+// 		if repoName := os.Getenv("PORTER_REPO_NAME"); repoName != "" {
+// 			if repoOwner := os.Getenv("PORTER_REPO_OWNER"); repoOwner != "" {
+// 				repoSuffix = strings.ToLower(strings.ReplaceAll(fmt.Sprintf("%s-%s", repoOwner, repoName), "_", "-"))
+// 			}
+// 		}
+
+// 		var registryURL string
+
+// 		if buildConfig.ImageRepoURI != nil {
+// 			registryURL = buildConfig.ImageRepoURI.GetValue()
+// 		}
+
+// 		if registryURL == "" {
+// 			regList, err := d.APIClient.ListRegistries(context.Background(), config.GetCLIConfig().Project)
+
+// 			if err != nil {
+// 				return fmt.Errorf("error fetching list of registries while trying to choose registry to deploy new"+
+// 					" image for app '%s': %w", resource.GetValue().Name.GetValue(), err)
+// 			}
+
+// 			if len(*regList) == 0 {
+// 				return fmt.Errorf("no registries linked with project, needed to deploy new image for app '%s'",
+// 					resource.GetValue().Name.GetValue())
+// 			} else {
+// 				registryURL = (*regList)[0].URL
+// 			}
+// 		}
+
+// 		createAgent := &deploy.CreateAgent{
+// 			Client: d.APIClient,
+// 			CreateOpts: &deploy.CreateOpts{
+// 				SharedOpts:  sharedOpts,
+// 				Kind:        resource.GetValue().Type.GetValue(),
+// 				ReleaseName: resource.GetValue().Name.GetValue(),
+// 				RegistryURL: registryURL,
+// 				RepoSuffix:  repoSuffix,
+// 			},
+// 		}
+
+// 		if buildConfig.Method.GetValue() == "registry" {
+// 			flattenedDeployMap := make(map[string]any)
+
+// 			for k, v := range resource.GetValue().Deploy.GetValue() {
+// 				flattenedDeployMap[k.GetValue()] = v.GetValue()
+// 			}
+
+// 			values := &porterWebChartValues{}
+
+// 			// delete the aliases from the deploy section
+// 			delete(flattenedDeployMap, "command")
+// 			delete(flattenedDeployMap, "cpu")
+// 			delete(flattenedDeployMap, "memory")
+
+// 			// replace alias values to the original expect yaml values
+// 			values.Container.Command = appDeploy.Command
+// 			values.Container.Env.Build = flattenedBuildEnv
+// 			values.Container.Env.Normal = appDeploy.Env
+// 			// values.Container.Env.Synced
+// 			values.Resources.Requests.CPU = appDeploy.CPU
+// 			values.Resources.Requests.Memory = appDeploy.Memory
+// 			if len(appDeploy.Hosts) > 0 {
+// 				values.Ingress.CustomDomain = true
+// 				values.Ingress.Hosts = appDeploy.Hosts
+// 			}
+
+// 			overrideValues := make(map[string]any)
+
+// 			err = mapstructure.Decode(values, &overrideValues)
+
+// 			if err != nil {
+// 				return err // FIXME
+// 			}
+
+// 			_, err := createAgent.CreateFromRegistry("", overrideValues)
+
+// 			if err != nil {
+// 				return fmt.Errorf("[porter.yaml v2][app:%s] error creating job: %w", resource.GetValue().Name.GetValue(), err)
+// 			}
+// 		} else if oneOf(buildConfig.Method.GetValue(), "pack", "docker") {
+// 			_, err := createAgent.CreateFromDocker(nil, "", nil)
+
+// 			if err != nil {
+// 				return fmt.Errorf("[porter.yaml v2][app:%s] error creating job: %w", resource.GetValue().Name.GetValue(), err)
+// 			}
+// 		} else {
+// 			// this should not happen
+// 			return fmt.Errorf("internal error: please let the Porter team know about this and quote the following "+
+// 				"error:\n-----\nERROR: build method was not one of 'pack', 'docker', 'registry' for app '%s'",
+// 				resource.GetValue().Name.GetValue())
+// 		}
+// 	}
+
+// 	return nil
+// }
+
+// // fetching the image tag works in 3 steps
+// //   - read PORTER_TAG env var
+// //   - read the git SHA from the current directory
+// //   - default to 'latest' tag
+// func getImageTag() string {
+// 	tag := os.Getenv("PORTER_TAG")
+
+// 	if tag == "" {
+// 		commit, err := git.LastCommit()
+
+// 		if err == nil {
+// 			tag = commit.Sha[:7]
+
+// 			color.New(color.FgBlue).Printf("[porter.yaml v2] PORTER_TAG not defined, falling back to image tag '%s'"+
+// 				" from git SHA\n", tag)
+// 		}
+// 	} else {
+// 		color.New(color.FgBlue).Printf("[porter.yaml v2] Using image tag '%s' from PORTER_TAG environment variable\n", tag)
+// 	}
+
+// 	if tag == "" {
+// 		color.New(color.FgBlue).Println("[porter.yaml v2] PORTER_TAG not defined, not a git repository, falling back" +
+// 			" to image tag 'latest'")
+
+// 		tag = "latest"
+// 	}
+
+// 	return tag
+// }

+ 25 - 0
cli/cmd/preview/v2beta1/helm_chart.go

@@ -0,0 +1,25 @@
+package v2beta1
+
+func (c *HelmChart) GetName() string {
+	if c == nil || c.Name == nil {
+		return ""
+	}
+
+	return *c.Name
+}
+
+func (c *HelmChart) GetURL() string {
+	if c == nil || c.URL == nil {
+		return ""
+	}
+
+	return *c.URL
+}
+
+func (c *HelmChart) GetVersion() string {
+	if c == nil || c.Version == nil {
+		return ""
+	}
+
+	return *c.Version
+}

+ 62 - 0
cli/cmd/preview/v2beta1/types.go

@@ -0,0 +1,62 @@
+package v2beta1
+
+// type Variable struct {
+// 	Name   *string `yaml:"name" validate:"required,unique"`
+// 	Value  *string `yaml:"value" validate:"required_if=Random false"`
+// 	Once   *bool   `yaml:"once"`
+// 	Random *bool   `yaml:"random"`
+// 	Length *uint   `yaml:"length"`
+// }
+
+// type EnvGroup struct {
+// 	Name      *string `yaml:"name" validate:"required"`
+// 	CloneFrom *string `yaml:"clone_from" validate:"required"`
+// }
+
+type BuildEnv struct {
+	Raw        map[*string]*string `yaml:"raw"`
+	ImportFrom []*string           `yaml:"import_from"`
+}
+
+type Build struct {
+	Name       *string   `yaml:"name" validate:"required"`
+	Context    *string   `yaml:"context" validate:"dir"`
+	Method     *string   `yaml:"method" validate:"required,oneof=pack docker registry"`
+	Builder    *string   `yaml:"builder" validate:"required_if=Method pack"`
+	Buildpacks []*string `yaml:"buildpacks"`
+	Dockerfile *string   `yaml:"dockerfile" validate:"required_if=Method docker"`
+	Image      *string   `yaml:"image" validate:"required_if=Method registry"`
+	Env        *BuildEnv `yaml:"env"`
+	// UseCache   *bool     `yaml:"use_cache"`
+}
+
+type HelmChart struct {
+	URL     *string `yaml:"url" validate:"url"`
+	Name    *string `yaml:"name" validate:"required"`
+	Version *string `yaml:"version"`
+}
+
+type AppResource struct {
+	Name      *string    `yaml:"name" validate:"required"`
+	DependsOn []*string  `yaml:"depends_on"`
+	Chart     *HelmChart `yaml:"helm_chart"`
+	BuildRef  *string    `yaml:"build_ref"`
+	// Deploy     map[*string]*any `yaml:"deploy"`
+	HelmValues map[string]any `yaml:"helm_values"`
+}
+
+type AddonResource struct {
+	Name       *string        `yaml:"name" validate:"required"`
+	DependsOn  []*string      `yaml:"depends_on"`
+	Chart      *HelmChart     `yaml:"helm_chart" validate:"required"`
+	HelmValues map[string]any `yaml:"helm_values"`
+}
+
+type PorterYAML struct {
+	Version *string `yaml:"version"`
+	// Variables []*Variable `yaml:"variables"`
+	// EnvGroups []*EnvGroup `yaml:"env_groups"`
+	Builds []*Build         `yaml:"builds"`
+	Apps   []*AppResource   `yaml:"apps"`
+	Addons []*AddonResource `yaml:"addons"`
+}

+ 57 - 0
cli/cmd/preview/v2beta1/utils.go

@@ -0,0 +1,57 @@
+package v2beta1
+
+import (
+	"crypto/rand"
+	"fmt"
+
+	"github.com/fatih/color"
+)
+
+type MessageLevel string
+
+const (
+	Warning MessageLevel = "WARN"
+	Error   MessageLevel = "ERR"
+	Success MessageLevel = "OK"
+	Info    MessageLevel = "INFO"
+)
+
+func composePreviewMessage(msg string, level MessageLevel) string {
+	return fmt.Sprintf("[porter.yaml v2beta1][%s] -- %s", level, msg)
+}
+
+func printWarningMessage(msg string) {
+	color.New(color.FgYellow).Printf(fmt.Sprintf("%s\n", composePreviewMessage(msg, Warning)))
+}
+
+func printErrorMessage(msg string) {
+	color.New(color.FgRed).Printf(fmt.Sprintf("%s\n", composePreviewMessage(msg, Error)))
+}
+
+func printSuccessMessage(msg string) {
+	color.New(color.FgGreen).Printf(fmt.Sprintf("%s\n", composePreviewMessage(msg, Success)))
+}
+
+func printInfoMessage(msg string) {
+	color.New(color.FgBlue).Printf(fmt.Sprintf("%s\n", composePreviewMessage(msg, Info)))
+}
+
+func booleanptr(b bool) *bool {
+	copy := b
+	return &copy
+}
+
+func stringptr(s string) *string {
+	copy := s
+	return &copy
+}
+
+func randomString(length uint, charset string) string {
+	ll := len(charset)
+	b := make([]byte, length)
+	rand.Read(b) // generates len(b) random bytes
+	for i := uint(0); i < length; i++ {
+		b[i] = charset[int(b[i])%ll]
+	}
+	return string(b)
+}