2
0
Mohammed Nafees 3 жил өмнө
parent
commit
a8d75192ce

+ 41 - 33
cli/cmd/apply.go

@@ -123,6 +123,8 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 		return fmt.Errorf("error unmarshaling porter.yaml: %w", err)
 	}
 
+	var resGroup *switchboardTypes.ResourceGroup
+
 	if previewVersion.Version == "v2beta1" {
 		ns := os.Getenv("PORTER_NAMESPACE")
 
@@ -132,7 +134,13 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 			return err
 		}
 
-		return applier.Apply()
+		resGroup, err = applier.DowngradeToV1()
+
+		if err != nil {
+			return err
+		}
+
+		return nil
 	} else if previewVersion.Version == "v1" {
 		if _, ok := os.LookupEnv("PORTER_VALIDATE_YAML"); ok {
 			err := applyValidate()
@@ -142,54 +150,54 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 			}
 		}
 
-		resGroup, err := parser.ParseRawBytes(fileBytes)
+		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()
-
-		if err != nil {
-			return fmt.Errorf("error getting working directory: %w", err)
-		}
+	basePath, err := os.Getwd()
 
-		worker := switchboardWorker.NewWorker()
-		worker.RegisterDriver("deploy", NewDeployDriver)
-		worker.RegisterDriver("build-image", preview.NewBuildDriver)
-		worker.RegisterDriver("push-image", preview.NewPushDriver)
-		worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
-		worker.RegisterDriver("random-string", preview.NewRandomStringDriver)
-		worker.RegisterDriver("env-group", preview.NewEnvGroupDriver)
-		worker.RegisterDriver("os-env", preview.NewOSEnvDriver)
+	if err != nil {
+		return fmt.Errorf("error getting working directory: %w", err)
+	}
 
-		worker.SetDefaultDriver("deploy")
+	worker := switchboardWorker.NewWorker()
+	worker.RegisterDriver("deploy", NewDeployDriver)
+	worker.RegisterDriver("build-image", preview.NewBuildDriver)
+	worker.RegisterDriver("push-image", preview.NewPushDriver)
+	worker.RegisterDriver("update-config", preview.NewUpdateConfigDriver)
+	worker.RegisterDriver("random-string", preview.NewRandomStringDriver)
+	worker.RegisterDriver("env-group", preview.NewEnvGroupDriver)
+	worker.RegisterDriver("os-env", preview.NewOSEnvDriver)
 
-		if hasDeploymentHookEnvVars() {
-			deplNamespace := os.Getenv("PORTER_NAMESPACE")
+	worker.SetDefaultDriver("deploy")
 
-			if deplNamespace == "" {
-				return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
-			}
+	if hasDeploymentHookEnvVars() {
+		deplNamespace := os.Getenv("PORTER_NAMESPACE")
 
-			deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
+		if deplNamespace == "" {
+			return fmt.Errorf("namespace must be set by PORTER_NAMESPACE")
+		}
 
-			if err != nil {
-				return fmt.Errorf("error creating deployment hook: %w", err)
-			}
+		deploymentHook, err := NewDeploymentHook(client, resGroup, deplNamespace)
 
-			worker.RegisterHook("deployment", deploymentHook)
+		if err != nil {
+			return fmt.Errorf("error creating deployment hook: %w", err)
 		}
 
-		cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
-		worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
-
-		return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
-			BasePath: basePath,
-		})
+		worker.RegisterHook("deployment", deploymentHook)
 	}
 
-	return fmt.Errorf("unknown porter.yaml version: %s", previewVersion.Version)
+	cloneEnvGroupHook := NewCloneEnvGroupHook(client, resGroup)
+	worker.RegisterHook("cloneenvgroup", cloneEnvGroupHook)
+
+	return worker.Apply(resGroup, &switchboardTypes.ApplyOpts{
+		BasePath: basePath,
+	})
 }
 
 func applyValidate() error {

+ 235 - 195
cli/cmd/preview/v2beta1/apply.go

@@ -3,20 +3,18 @@ package v2beta1
 import (
 	"context"
 	"fmt"
-	"os"
-	"strings"
 
 	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/switchboard/pkg/types"
 	"gopkg.in/yaml.v3"
 )
 
-const (
-	constantsEnvGroup = "preview-env-constants"
+// const (
+// 	constantsEnvGroup = "preview-env-constants"
 
-	defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~`!@#$%^&*()_+-={}[]"
-)
+// 	defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~`!@#$%^&*()_+-={}[]"
+// )
 
 type PreviewApplier struct {
 	apiClient *api.Client
@@ -24,9 +22,9 @@ type PreviewApplier struct {
 	namespace string
 	parsed    *PorterYAML
 
-	variablesMap map[string]string
-	osEnv        map[string]string
-	envGroups    map[string]*apiTypes.EnvGroup
+	// variablesMap map[string]string
+	// osEnv        map[string]string
+	// envGroups    map[string]*apiTypes.EnvGroup
 }
 
 func NewApplier(client *api.Client, raw []byte, namespace string) (*PreviewApplier, error) {
@@ -73,9 +71,9 @@ func validateCLIEnvironment(namespace string) error {
 		return fmt.Errorf("no cluster selected, please run 'porter config set-cluster' to select a cluster")
 	}
 
-	if namespace == "" {
-		return fmt.Errorf("no namespace provided, please set the PORTER_NAMESPACE environment variable")
-	}
+	// if namespace == "" {
+	// 	printInfoMessage("no namespace provided, falling back to namespace 'default'")
+	// }
 
 	return nil
 }
@@ -108,9 +106,9 @@ func (a *PreviewApplier) Apply() error {
 	}
 
 	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)
+		// 	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"+
@@ -143,202 +141,244 @@ func (a *PreviewApplier) Apply() error {
 	return 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_")
-			}
+func (a *PreviewApplier) DowngradeToV1() (*types.ResourceGroup, error) {
+	err := a.Apply()
 
-			if k == "" {
-				printWarningMessage(fmt.Sprintf("Ignoring invalid OS environment variable '%s'", kCopy))
-			}
-
-			osEnv[k] = v
-		}
+	if err != nil {
+		return nil, err
 	}
 
-	a.osEnv = osEnv
-
-	return nil
-}
-
-func (a *PreviewApplier) processVariables() error {
-	printInfoMessage("Processing variables")
-
-	constantsMap := make(map[string]string)
-	variablesMap := make(map[string]string)
+	v1File := &types.ResourceGroup{
+		Version: "v1",
+	}
 
-	for _, v := range a.parsed.Variables {
-		if v == nil {
+	for _, b := range a.parsed.Builds {
+		if b == 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,
-			},
-		)
+		bi, err := b.getV1BuildImage()
 
 		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
+			return nil, err
 		}
-	}
-
-	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
-		},
-	)
+		pi, err := b.getV1PushImage()
 
-	if err != nil {
-		if strings.Contains(err.Error(), "env group not found") {
-			return booleanptr(false), nil
+		if err != nil {
+			return nil, err
 		}
 
-		return nil, err
-	}
-
-	if _, ok := apiResponse.Variables[name]; ok {
-		return booleanptr(true), nil
+		v1File.Resources = append(v1File.Resources, bi, pi)
 	}
 
-	return booleanptr(false), nil
-}
-
-func (a *PreviewApplier) processEnvGroups() error {
-	printInfoMessage("Processing env groups")
+	// fileBytes, err := yaml.Marshal(v1File)
 
-	for _, eg := range a.parsed.EnvGroups {
-		if eg == nil {
-			continue
-		}
-
-		if eg.Name == nil || *eg.Name == "" {
-
-		}
+	// if err != nil {
+	// 	return nil, err
+	// }
 
-		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,
-			}
-		}
-	}
+	// fmt.Println(string(fileBytes))
 
-	return nil
+	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
+// }

+ 28 - 2
cli/cmd/preview/v2beta1/build.go

@@ -119,7 +119,7 @@ func (b *Build) GetEnvGroups() []string {
 	return eg
 }
 
-func (b *Build) getV1Build() (*types.Resource, error) {
+func (b *Build) getV1BuildImage() (*types.Resource, error) {
 	config := &preview.BuildDriverConfig{}
 
 	if b.GetMethod() == "pack" {
@@ -157,7 +157,7 @@ func (b *Build) getV1Build() (*types.Resource, error) {
 	}
 
 	return &types.Resource{
-		Name:   b.GetName(),
+		Name:   fmt.Sprintf("%s-build-image", b.GetName()),
 		Driver: "build-image",
 		Source: map[string]any{
 			"name": "web",
@@ -168,3 +168,29 @@ func (b *Build) getV1Build() (*types.Resource, error) {
 		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
+}

+ 17 - 17
cli/cmd/preview/v2beta1/types.go

@@ -1,17 +1,17 @@
 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 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 EnvGroup struct {
+// 	Name      *string `yaml:"name" validate:"required"`
+// 	CloneFrom *string `yaml:"clone_from" validate:"required"`
+// }
 
 type BuildEnv struct {
 	Raw        map[*string]*string `yaml:"raw"`
@@ -41,10 +41,10 @@ type Resource struct {
 }
 
 type PorterYAML struct {
-	Version   *string     `yaml:"version"`
-	Variables []*Variable `yaml:"variables"`
-	EnvGroups []*EnvGroup `yaml:"env_groups"`
-	Builds    []*Build    `yaml:"builds"`
-	Apps      []*Resource `yaml:"apps"`
-	Addons    []*Resource `yaml:"addons"`
+	Version *string `yaml:"version"`
+	// Variables []*Variable `yaml:"variables"`
+	// EnvGroups []*EnvGroup `yaml:"env_groups"`
+	Builds []*Build    `yaml:"builds"`
+	Apps   []*Resource `yaml:"apps"`
+	Addons []*Resource `yaml:"addons"`
 }