소스 검색

Merge pull request #2483 from porter-dev/nafees/pr-env-validator

[POR-706] Validator for preview environments
abelanger5 3 년 전
부모
커밋
fa7463c772

+ 128 - 0
api/server/handlers/environment/validate_porter_yaml.go

@@ -0,0 +1,128 @@
+package environment
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/google/go-github/v41/github"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/requestutils"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/integrations/preview"
+	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
+)
+
+type ValidatePorterYAMLHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewValidatePorterYAMLHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *ValidatePorterYAMLHandler {
+	return &ValidatePorterYAMLHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *ValidatePorterYAMLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+
+	envID, reqErr := requestutils.GetURLParamUint(r, "environment_id")
+
+	if reqErr != nil {
+		c.HandleAPIError(w, r, reqErr)
+		return
+	}
+
+	req := &types.ValidatePorterYAMLRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, req); !ok {
+		return
+	}
+
+	env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, envID)
+
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			c.HandleAPIError(w, r, apierrors.NewErrNotFound(fmt.Errorf("no such environment with ID: %d", envID)))
+			return
+		}
+
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error reading environment with ID: %d. Error: %w", envID, err)))
+		return
+	}
+
+	ghClient, err := getGithubClientFromEnvironment(c.Config(), env)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	res := &types.ValidatePorterYAMLResponse{
+		Errors: []string{},
+	}
+
+	if req.Branch == "" { // get the default branch name
+		repo, _, err := ghClient.Repositories.Get(r.Context(), env.GitRepoOwner, env.GitRepoName)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+
+		req.Branch = repo.GetDefaultBranch()
+	}
+
+	fileContents, _, ghResp, err := ghClient.Repositories.GetContents(
+		context.Background(), env.GitRepoOwner, env.GitRepoName, "porter.yaml",
+		&github.RepositoryContentGetOptions{
+			Ref: req.Branch,
+		},
+	)
+
+	if ghResp.StatusCode == 404 {
+		res.Errors = append(res.Errors, preview.ErrNoPorterYAMLFile.Error())
+		c.WriteResult(w, r, res)
+		return
+	}
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	contents, err := fileContents.GetContent()
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if strings.TrimSpace(contents) == "" {
+		res.Errors = append(res.Errors, preview.ErrEmptyPorterYAMLFile.Error())
+		c.WriteResult(w, r, res)
+		return
+	}
+
+	for _, err := range preview.Validate(contents) {
+		if err != nil {
+			res.Errors = append(res.Errors, err.Error())
+		}
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 30 - 1
api/server/router/cluster.go

@@ -346,7 +346,7 @@ func getClusterRoutes(
 			Router:   r,
 		})
 
-		// PATCH /api/projects/{project_id}/clusters/{cluster_id}/environment/{environment_id}/toggle_new_comment -> environment.NewToggleNewCommentHandler
+		// PATCH /api/projects/{project_id}/clusters/{cluster_id}/environments/{environment_id}/toggle_new_comment -> environment.NewToggleNewCommentHandler
 		toggleNewCommentEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{
 				Verb:   types.APIVerbUpdate,
@@ -375,6 +375,35 @@ func getClusterRoutes(
 			Router:   r,
 		})
 
+		// GET /api/projects/{project_id}/clusters/{cluster_id}/environments/{environment_id}/validate_porter_yaml -> environment.NewValidatePorterYAMLHandler
+		validtatePorterYAMLEndpoint := factory.NewAPIEndpoint(
+			&types.APIRequestMetadata{
+				Verb:   types.APIVerbGet,
+				Method: types.HTTPVerbGet,
+				Path: &types.Path{
+					Parent:       basePath,
+					RelativePath: relPath + "/environments/{environment_id}/validate_porter_yaml",
+				},
+				Scopes: []types.PermissionScope{
+					types.UserScope,
+					types.ProjectScope,
+					types.ClusterScope,
+				},
+			},
+		)
+
+		validatePorterYAMLHandler := environment.NewValidatePorterYAMLHandler(
+			config,
+			factory.GetDecoderValidator(),
+			factory.GetResultWriter(),
+		)
+
+		routes = append(routes, &router.Route{
+			Endpoint: validtatePorterYAMLEndpoint,
+			Handler:  validatePorterYAMLHandler,
+			Router:   r,
+		})
+
 		// GET /api/projects/{project_id}/clusters/{cluster_id}/deployments -> environment.NewListDeploymentsByClusterHandler
 		listDeploymentsEndpoint := factory.NewAPIEndpoint(
 			&types.APIRequestMetadata{

+ 8 - 0
api/types/environment.go

@@ -129,3 +129,11 @@ type ToggleNewCommentRequest struct {
 }
 
 type ListEnvironmentsResponse []*Environment
+
+type ValidatePorterYAMLRequest struct {
+	Branch string `schema:"branch"`
+}
+
+type ValidatePorterYAMLResponse struct {
+	Errors []string `json:"errors"`
+}

+ 59 - 44
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"
+	previewInt "github.com/porter-dev/porter/internal/integrations/preview"
 	"github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/porter-dev/switchboard/pkg/drivers"
 	"github.com/porter-dev/switchboard/pkg/models"
@@ -71,16 +72,43 @@ applying a configuration:
 	},
 }
 
+// applyValidateCmd represents the "porter apply validate" command when called
+// with a porter.yaml file as an argument
+var applyValidateCmd = &cobra.Command{
+	Use:   "validate",
+	Short: "Validates a porter.yaml",
+	Run: func(*cobra.Command, []string) {
+		err := applyValidate()
+
+		if err != nil {
+			color.New(color.FgRed).Fprintf(os.Stderr, "Error: %s\n", err.Error())
+			os.Exit(1)
+		} else {
+			color.New(color.FgGreen).Printf("The porter.yaml file is valid!\n")
+		}
+	},
+}
+
 var porterYAML string
 
 func init() {
 	rootCmd.AddCommand(applyCmd)
 
-	applyCmd.Flags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
+	applyCmd.AddCommand(applyValidateCmd)
+
+	applyCmd.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml")
 	applyCmd.MarkFlagRequired("file")
 }
 
 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 {
@@ -134,6 +162,28 @@ func apply(_ *types.GetAuthenticatedUserResponse, client *api.Client, _ []string
 	})
 }
 
+func applyValidate() error {
+	fileBytes, err := ioutil.ReadFile(porterYAML)
+
+	if err != nil {
+		return fmt.Errorf("error reading porter.yaml: %w", err)
+	}
+
+	validationErrors := previewInt.Validate(string(fileBytes))
+
+	if len(validationErrors) > 0 {
+		errString := "the following error(s) were found while validating the porter.yaml file:"
+
+		for _, err := range validationErrors {
+			errString += "\n- " + strings.ReplaceAll(err.Error(), "\n\n*", "\n  *")
+		}
+
+		return fmt.Errorf(errString)
+	}
+
+	return nil
+}
+
 func hasDeploymentHookEnvVars() bool {
 	if ghIDStr := os.Getenv("PORTER_GIT_INSTALLATION_ID"); ghIDStr == "" {
 		return false
@@ -170,32 +220,9 @@ func hasDeploymentHookEnvVars() bool {
 	return true
 }
 
-type ApplicationConfig struct {
-	WaitForJob bool
-
-	// If set to true, this does not run an update, it only creates the initial application and job,
-	// skipping subsequent updates
-	OnlyCreate bool
-
-	Build struct {
-		UseCache   bool `mapstructure:"use_cache"`
-		Method     string
-		Context    string
-		Dockerfile string
-		Image      string
-		Builder    string
-		Buildpacks []string
-		Env        map[string]string
-	}
-
-	EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
-
-	Values map[string]interface{}
-}
-
 type DeployDriver struct {
-	source      *preview.Source
-	target      *preview.Target
+	source      *previewInt.Source
+	target      *previewInt.Target
 	output      map[string]interface{}
 	lookupTable *map[string]drivers.Driver
 	logger      *zerolog.Logger
@@ -233,11 +260,6 @@ func (d *DeployDriver) ShouldApply(_ *models.Resource) bool {
 
 func (d *DeployDriver) Apply(resource *models.Resource) (*models.Resource, error) {
 	client := config.GetAPIClient()
-	name := resource.Name
-
-	if name == "" {
-		return nil, fmt.Errorf("empty resource name")
-	}
 
 	_, err := client.GetRelease(
 		context.Background(),
@@ -331,13 +353,6 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 		return nil, err
 	}
 
-	method := appConfig.Build.Method
-
-	if method != "pack" && method != "docker" && method != "registry" {
-		return nil, fmt.Errorf("for resource %s, config.build.method should either be \"docker\", \"pack\" or \"registry\"",
-			resourceName)
-	}
-
 	fullPath, err := filepath.Abs(appConfig.Build.Context)
 
 	if err != nil {
@@ -382,7 +397,7 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 		LocalPath:       fullPath,
 		LocalDockerfile: appConfig.Build.Dockerfile,
 		OverrideTag:     tag,
-		Method:          deploy.DeployBuildType(method),
+		Method:          deploy.DeployBuildType(appConfig.Build.Method),
 		EnvGroups:       appConfig.EnvGroups,
 		UseCache:        appConfig.Build.UseCache,
 	}
@@ -447,7 +462,7 @@ func (d *DeployDriver) applyApplication(resource *models.Resource, client *api.C
 	return resource, err
 }
 
-func (d *DeployDriver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
+func (d *DeployDriver) createApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*models.Resource, error) {
 	// create new release
 	color.New(color.FgGreen).Printf("Creating %s release: %s\n", d.source.Name, resource.Name)
 
@@ -533,7 +548,7 @@ func (d *DeployDriver) createApplication(resource *models.Resource, client *api.
 	return resource, handleSubdomainCreate(subdomain, err)
 }
 
-func (d *DeployDriver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *ApplicationConfig) (*models.Resource, error) {
+func (d *DeployDriver) updateApplication(resource *models.Resource, client *api.Client, sharedOpts *deploy.SharedOpts, appConf *previewInt.ApplicationConfig) (*models.Resource, error) {
 	color.New(color.FgGreen).Println("Updating existing release:", resource.Name)
 
 	if len(appConf.Build.Env) > 0 {
@@ -621,7 +636,7 @@ func (d *DeployDriver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 }
 
-func (d *DeployDriver) getApplicationConfig(resource *models.Resource) (*ApplicationConfig, error) {
+func (d *DeployDriver) getApplicationConfig(resource *models.Resource) (*previewInt.ApplicationConfig, error) {
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
@@ -632,7 +647,7 @@ func (d *DeployDriver) getApplicationConfig(resource *models.Resource) (*Applica
 		return nil, err
 	}
 
-	appConf := &ApplicationConfig{}
+	appConf := &previewInt.ApplicationConfig{}
 
 	err = mapstructure.Decode(populatedConf, appConf)
 
@@ -995,7 +1010,7 @@ func (t *CloneEnvGroupHook) PreApply() error {
 			continue
 		}
 
-		appConf := &ApplicationConfig{}
+		appConf := &previewInt.ApplicationConfig{}
 
 		err := mapstructure.Decode(res.Config, &appConf)
 		if err != nil {

+ 6 - 26
cli/cmd/preview/build_image_driver.go

@@ -14,31 +14,15 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/docker"
+	"github.com/porter-dev/porter/internal/integrations/preview"
 	"github.com/porter-dev/switchboard/pkg/drivers"
 	"github.com/porter-dev/switchboard/pkg/models"
 )
 
-type BuildDriverConfig struct {
-	Build struct {
-		UsePackCache bool `mapstructure:"use_pack_cache"`
-		Method       string
-		Context      string
-		Dockerfile   string
-		Builder      string
-		Buildpacks   []string
-		Image        string
-		Env          map[string]string
-	}
-
-	EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
-
-	Values map[string]interface{}
-}
-
 type BuildDriver struct {
-	source      *Source
-	target      *Target
-	config      *BuildDriverConfig
+	source      *preview.Source
+	target      *preview.Target
+	config      *preview.BuildDriverConfig
 	lookupTable *map[string]drivers.Driver
 	output      map[string]interface{}
 }
@@ -61,10 +45,6 @@ func NewBuildDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (
 		return nil, err
 	}
 
-	if target.AppName == "" {
-		return nil, fmt.Errorf("target app_name is missing")
-	}
-
 	driver.target = target
 
 	return driver, nil
@@ -335,7 +315,7 @@ func (d *BuildDriver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 }
 
-func (d *BuildDriver) getConfig(resource *models.Resource) (*BuildDriverConfig, error) {
+func (d *BuildDriver) getConfig(resource *models.Resource) (*preview.BuildDriverConfig, error) {
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
@@ -346,7 +326,7 @@ func (d *BuildDriver) getConfig(resource *models.Resource) (*BuildDriverConfig,
 		return nil, err
 	}
 
-	config := &BuildDriverConfig{}
+	config := &preview.BuildDriverConfig{}
 
 	err = mapstructure.Decode(populatedConf, config)
 

+ 5 - 8
cli/cmd/preview/env_group_driver.go

@@ -8,19 +8,16 @@ import (
 	"github.com/mitchellh/mapstructure"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/internal/integrations/preview"
 	"github.com/porter-dev/switchboard/pkg/drivers"
 	"github.com/porter-dev/switchboard/pkg/models"
 )
 
-type EnvGroupDriverConfig struct {
-	EnvGroups []*types.EnvGroup `mapstructure:"env_groups"`
-}
-
 type EnvGroupDriver struct {
 	output      map[string]interface{}
 	lookupTable *map[string]drivers.Driver
-	target      *Target
-	config      *EnvGroupDriverConfig
+	target      *preview.Target
+	config      *preview.EnvGroupDriverConfig
 }
 
 func NewEnvGroupDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
@@ -112,7 +109,7 @@ func (d *EnvGroupDriver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 }
 
-func (d *EnvGroupDriver) getConfig(resource *models.Resource) (*EnvGroupDriverConfig, error) {
+func (d *EnvGroupDriver) getConfig(resource *models.Resource) (*preview.EnvGroupDriverConfig, error) {
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
@@ -123,7 +120,7 @@ func (d *EnvGroupDriver) getConfig(resource *models.Resource) (*EnvGroupDriverCo
 		return nil, err
 	}
 
-	config := &EnvGroupDriverConfig{}
+	config := &preview.EnvGroupDriverConfig{}
 
 	err = mapstructure.Decode(populatedConf, config)
 

+ 5 - 15
cli/cmd/preview/push_image_driver.go

@@ -11,20 +11,14 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/docker"
+	"github.com/porter-dev/porter/internal/integrations/preview"
 	"github.com/porter-dev/switchboard/pkg/drivers"
 	"github.com/porter-dev/switchboard/pkg/models"
 )
 
-type PushDriverConfig struct {
-	Push struct {
-		UsePackCache bool `mapstructure:"use_pack_cache"`
-		Image        string
-	}
-}
-
 type PushDriver struct {
-	target      *Target
-	config      *PushDriverConfig
+	target      *preview.Target
+	config      *preview.PushDriverConfig
 	lookupTable *map[string]drivers.Driver
 	output      map[string]interface{}
 }
@@ -40,10 +34,6 @@ func NewPushDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (d
 		return nil, err
 	}
 
-	if target.AppName == "" {
-		return nil, fmt.Errorf("target app_name is missing")
-	}
-
 	driver.target = target
 
 	return driver, nil
@@ -157,7 +147,7 @@ func (d *PushDriver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 }
 
-func (d *PushDriver) getConfig(resource *models.Resource) (*PushDriverConfig, error) {
+func (d *PushDriver) getConfig(resource *models.Resource) (*preview.PushDriverConfig, error) {
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
@@ -168,7 +158,7 @@ func (d *PushDriver) getConfig(resource *models.Resource) (*PushDriverConfig, er
 		return nil, err
 	}
 
-	config := &PushDriverConfig{}
+	config := &preview.PushDriverConfig{}
 
 	err = mapstructure.Decode(populatedConf, config)
 

+ 4 - 8
cli/cmd/preview/random_string_driver.go

@@ -4,6 +4,7 @@ import (
 	"crypto/rand"
 
 	"github.com/mitchellh/mapstructure"
+	"github.com/porter-dev/porter/internal/integrations/preview"
 	"github.com/porter-dev/switchboard/pkg/drivers"
 	"github.com/porter-dev/switchboard/pkg/models"
 )
@@ -11,14 +12,9 @@ import (
 const defaultCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
 const lowerCharset = "abcdefghijklmnopqrstuvwxyz"
 
-type RandomStringDriverConfig struct {
-	Length int
-	Lower  bool
-}
-
 type RandomStringDriver struct {
 	output map[string]interface{}
-	config *RandomStringDriverConfig
+	config *preview.RandomStringDriverConfig
 }
 
 func NewRandomStringDriver(resource *models.Resource, opts *drivers.SharedDriverOpts) (drivers.Driver, error) {
@@ -26,7 +22,7 @@ func NewRandomStringDriver(resource *models.Resource, opts *drivers.SharedDriver
 		output: make(map[string]interface{}),
 	}
 
-	driverConfig := &RandomStringDriverConfig{}
+	driverConfig := &preview.RandomStringDriverConfig{}
 
 	err := mapstructure.Decode(resource.Config, driverConfig)
 
@@ -54,7 +50,7 @@ func (d *RandomStringDriver) Apply(resource *models.Resource) (*models.Resource,
 		useCharset = lowerCharset
 	}
 
-	d.output["value"] = randomString(d.config.Length, useCharset)
+	d.output["value"] = randomString(int(d.config.Length), useCharset)
 
 	return resource, nil
 }

+ 6 - 26
cli/cmd/preview/update_config_driver.go

@@ -14,32 +14,16 @@ import (
 	"github.com/porter-dev/porter/cli/cmd/config"
 	"github.com/porter-dev/porter/cli/cmd/deploy"
 	"github.com/porter-dev/porter/cli/cmd/deploy/wait"
+	"github.com/porter-dev/porter/internal/integrations/preview"
 	"github.com/porter-dev/porter/internal/templater/utils"
 	"github.com/porter-dev/switchboard/pkg/drivers"
 	"github.com/porter-dev/switchboard/pkg/models"
 )
 
-type UpdateConfigDriverConfig struct {
-	WaitForJob bool
-
-	// If set to true, this does not run an update, it only creates the initial application and job,
-	// skipping subsequent updates
-	OnlyCreate bool
-
-	UpdateConfig struct {
-		Image string
-		Tag   string
-	} `mapstructure:"update_config"`
-
-	EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
-
-	Values map[string]interface{}
-}
-
 type UpdateConfigDriver struct {
-	source      *Source
-	target      *Target
-	config      *UpdateConfigDriverConfig
+	source      *preview.Source
+	target      *preview.Target
+	config      *preview.UpdateConfigDriverConfig
 	lookupTable *map[string]drivers.Driver
 	output      map[string]interface{}
 }
@@ -62,10 +46,6 @@ func NewUpdateConfigDriver(resource *models.Resource, opts *drivers.SharedDriver
 		return nil, err
 	}
 
-	if target.AppName == "" {
-		return nil, fmt.Errorf("target app_name is missing")
-	}
-
 	driver.target = target
 
 	return driver, nil
@@ -225,7 +205,7 @@ func (d *UpdateConfigDriver) Output() (map[string]interface{}, error) {
 	return d.output, nil
 }
 
-func (d *UpdateConfigDriver) getConfig(resource *models.Resource) (*UpdateConfigDriverConfig, error) {
+func (d *UpdateConfigDriver) getConfig(resource *models.Resource) (*preview.UpdateConfigDriverConfig, error) {
 	populatedConf, err := drivers.ConstructConfig(&drivers.ConstructConfigOpts{
 		RawConf:      resource.Config,
 		LookupTable:  *d.lookupTable,
@@ -236,7 +216,7 @@ func (d *UpdateConfigDriver) getConfig(resource *models.Resource) (*UpdateConfig
 		return nil, err
 	}
 
-	config := &UpdateConfigDriverConfig{}
+	config := &preview.UpdateConfigDriverConfig{}
 
 	err = mapstructure.Decode(populatedConf, config)
 

+ 5 - 19
cli/cmd/preview/utils.go

@@ -8,25 +8,11 @@ import (
 
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/cli/cmd/config"
+	"github.com/porter-dev/porter/internal/integrations/preview"
 )
 
-type Source struct {
-	Name          string
-	Repo          string
-	Version       string
-	IsApplication bool
-	SourceValues  map[string]interface{}
-}
-
-type Target struct {
-	AppName   string
-	Project   uint
-	Cluster   uint
-	Namespace string
-}
-
-func GetSource(resourceName string, input map[string]interface{}) (*Source, error) {
-	output := &Source{}
+func GetSource(resourceName string, input map[string]interface{}) (*preview.Source, error) {
+	output := &preview.Source{}
 
 	// first read from env vars
 	output.Name = os.Getenv("PORTER_SOURCE_NAME")
@@ -113,8 +99,8 @@ func GetSource(resourceName string, input map[string]interface{}) (*Source, erro
 		resourceName, output.Name, output.Repo)
 }
 
-func GetTarget(resourceName string, input map[string]interface{}) (*Target, error) {
-	output := &Target{}
+func GetTarget(resourceName string, input map[string]interface{}) (*preview.Target, error) {
+	output := &preview.Target{}
 
 	// first read from env vars
 	if projectEnv := os.Getenv("PORTER_PROJECT"); projectEnv != "" {

+ 4 - 1
go.mod

@@ -39,7 +39,7 @@ require (
 	github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
 	github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198
 	github.com/pkg/errors v0.9.1
-	github.com/porter-dev/switchboard v0.0.0-20220628112428-7665a0121e4f
+	github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935
 	github.com/rs/zerolog v1.26.0
 	github.com/sendgrid/sendgrid-go v3.8.0+incompatible
 	github.com/spf13/cobra v1.5.0
@@ -119,7 +119,10 @@ require (
 	github.com/open-policy-agent/opa v0.44.0 // indirect
 	github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect
 	github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
+	github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect
+	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 // indirect
 	github.com/tchap/go-patricia/v2 v2.3.1 // indirect
+	github.com/tfkhsr/jsonschema v0.0.0-20180218143334-273afdd5a88c // indirect
 	github.com/xanzy/go-gitlab v0.68.0 // indirect
 	github.com/yashtewari/glob-intersection v0.1.0 // indirect
 	go.uber.org/goleak v1.1.12 // indirect

+ 8 - 0
go.sum

@@ -1729,6 +1729,8 @@ github.com/porter-dev/switchboard v0.0.0-20220416181342-416fc450addb h1:WNKCA31I
 github.com/porter-dev/switchboard v0.0.0-20220416181342-416fc450addb/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/porter-dev/switchboard v0.0.0-20220628112428-7665a0121e4f h1:REYJSDm2R3pM4mq88AlSBPIPhGiKFwiehe+GKZIc7Hc=
 github.com/porter-dev/switchboard v0.0.0-20220628112428-7665a0121e4f/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
+github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935 h1:hfb3nt3AJXIBbevu6ARTg9SdOkMP6WLbKBiG5hT5rcc=
+github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
 github.com/poy/onpar v0.0.0-20190519213022-ee068f8ea4d1/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU=
@@ -1844,6 +1846,10 @@ github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiB
 github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE=
 github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
 github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
+github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis=
+github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.1 h1:HNLA3HtUIROrQwG1cuu5EYuqk3UEoJ61Dr/9xkd6sok=
+github.com/santhosh-tekuri/jsonschema/v5 v5.0.1/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
 github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
@@ -1982,6 +1988,8 @@ github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0
 github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
 github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
 github.com/tetafro/godot v1.4.11/go.mod h1:LR3CJpxDVGlYOWn3ZZg1PgNZdTUvzsZWu8xaEohUpn8=
+github.com/tfkhsr/jsonschema v0.0.0-20180218143334-273afdd5a88c h1:FiJHojQ8AwCcltJnytC3Xkj37gW2WTzUzGl3AEYL+5U=
+github.com/tfkhsr/jsonschema v0.0.0-20180218143334-273afdd5a88c/go.mod h1:zhGMpmE6P0Eml0MgFIc5TljSWlr/hbNSmig8KiVEodo=
 github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
 github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
 github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=

+ 72 - 0
internal/integrations/preview/dep_resolver.go

@@ -0,0 +1,72 @@
+package preview
+
+import (
+	"fmt"
+
+	"github.com/porter-dev/switchboard/pkg/types"
+)
+
+type dependencyResolver struct {
+	resources  []*types.Resource
+	graph      map[string][]string
+	resolved   map[string]bool
+	unresolved map[string]bool
+}
+
+func newDependencyResolver(resources []*types.Resource) *dependencyResolver {
+	return &dependencyResolver{
+		resources:  resources,
+		graph:      make(map[string][]string),
+		resolved:   make(map[string]bool),
+		unresolved: make(map[string]bool),
+	}
+}
+
+func (r *dependencyResolver) Resolve() error {
+	if len(r.resources) > 0 {
+		// construct dependency graph
+		for _, resource := range r.resources {
+			// check for duplicate resource
+			if _, ok := r.graph[resource.Name]; ok {
+				return fmt.Errorf("duplicate resource detected: '%s'", resource.Name)
+			}
+
+			r.graph[resource.Name] = append(r.graph[resource.Name], resource.DependsOn...)
+		}
+
+		for _, resource := range r.resources {
+			err := r.depResolve(resource.Name)
+
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func (r *dependencyResolver) depResolve(name string) error {
+	r.unresolved[name] = true
+
+	for _, dep := range r.graph[name] {
+		if _, ok := r.graph[dep]; !ok {
+			return fmt.Errorf("for resource '%s': invalid dependency '%s'", name, dep)
+		}
+
+		if _, ok := r.resolved[dep]; !ok {
+			if _, ok = r.unresolved[dep]; ok {
+				return fmt.Errorf("circular depedency detected: '%s' -> '%s'", name, dep)
+			}
+			err := r.depResolve(dep)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	r.resolved[name] = true
+	delete(r.unresolved, name)
+
+	return nil
+}

+ 386 - 0
internal/integrations/preview/driver_validators.go

@@ -0,0 +1,386 @@
+package preview
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/docker/distribution/reference"
+	"github.com/mitchellh/mapstructure"
+	"github.com/porter-dev/switchboard/pkg/types"
+	"k8s.io/apimachinery/pkg/util/validation"
+)
+
+func commonValidator(resource *types.Resource) (*Source, *Target, error) {
+	source := &Source{}
+
+	err := mapstructure.Decode(resource.Source, source)
+
+	if err != nil {
+		return nil, nil, fmt.Errorf("for resource '%s': error parsing source: %w", resource.Name, err)
+	}
+
+	target := &Target{}
+
+	err = mapstructure.Decode(resource.Target, target)
+
+	if err != nil {
+		return nil, nil, fmt.Errorf("for resource '%s': error parsing target: %w", resource.Name, err)
+	}
+
+	return source, target, nil
+}
+
+func deployDriverValidator(resource *types.Resource) error {
+	source, _, err := commonValidator(resource)
+
+	if err != nil {
+		return err
+	}
+
+	if source.Name == "" {
+		return fmt.Errorf("for resource '%s': source name cannot be empty", resource.Name)
+	}
+
+	if source.Repo == "" {
+		if source.Name == "web" || source.Name == "worker" || source.Name == "job" {
+			source.Repo = "https://charts.getporter.dev"
+		} else {
+			source.Repo = "https://chart-addons.getporter.dev"
+		}
+	}
+
+	if source.Repo == "https://charts.getporter.dev" {
+		appConfig := &ApplicationConfig{}
+
+		err = mapstructure.Decode(resource.Config, appConfig)
+
+		if err != nil {
+			return fmt.Errorf("for resource '%s': error parsing config: %w", resource.Name, err)
+		}
+
+		if appConfig.Build.Method == "" {
+			return fmt.Errorf("for resource '%s': build method cannot be empty", resource.Name)
+		} else if appConfig.Build.Method != "docker" &&
+			appConfig.Build.Method != "pack" &&
+			appConfig.Build.Method != "registry" {
+			return fmt.Errorf("for resource '%s': build method must be one of 'docker', 'pack', or 'registry'", resource.Name)
+		}
+
+		if appConfig.Build.Method == "registry" {
+			if appConfig.Build.Image == "" {
+				return fmt.Errorf("for resource '%s': image cannot be empty when using the 'registry' build method",
+					resource.Name)
+			} else if !strings.Contains(appConfig.Build.Image, "{") {
+				if len(strings.Split(appConfig.Build.Image, ":")) != 2 {
+					return fmt.Errorf("for resource '%s': image must be in the format 'image:tag'", resource.Name)
+				}
+
+				// check for valid image
+				_, err := reference.ParseNamed(appConfig.Build.Image)
+
+				if err != nil {
+					return fmt.Errorf("for resource '%s': error parsing image: %w", resource.Name, err)
+				}
+			}
+		}
+
+		for _, eg := range appConfig.EnvGroups {
+			if errStrs := validation.IsDNS1123Label(eg.Name); len(errStrs) > 0 {
+				str := fmt.Sprintf("for resource '%s': invalid characters found in env group '%s' name:",
+					resource.Name, eg.Name)
+				for _, errStr := range errStrs {
+					str += fmt.Sprintf("\n  * %s", errStr)
+				}
+
+				return fmt.Errorf("%s", str)
+			}
+		}
+
+		if len(appConfig.Values) > 0 {
+			if source.Name == "web" {
+				err := validateWebChartValues(appConfig.Values)
+
+				if err != nil {
+					return fmt.Errorf("for resource '%s': error validating values for web deployment: %w",
+						resource.Name, err)
+				}
+			} else if source.Name == "worker" {
+				err := validateWorkerChartValues(appConfig.Values)
+
+				if err != nil {
+					return fmt.Errorf("for resource '%s': error validating values for worker deployment: %w",
+						resource.Name, err)
+				}
+			} else if source.Name == "job" {
+				err := validateJobChartValues(appConfig.Values)
+
+				if err != nil {
+					return fmt.Errorf("for resource '%s': error validating values for job deployment: %w",
+						resource.Name, err)
+				}
+			}
+		}
+	} else if source.Repo == "https://chart-addons.getporter.dev" {
+		if len(resource.Config) > 0 {
+			if source.Name == "postgresql" {
+				err := validatePostgresChartValues(resource.Config)
+
+				if err != nil {
+					return fmt.Errorf("for resource '%s': error validating values for postgresql deployment: %w",
+						resource.Name, err)
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+func buildImageDriverValidator(resource *types.Resource) error {
+	_, target, err := commonValidator(resource)
+
+	if err != nil {
+		return err
+	}
+
+	if target.AppName == "" {
+		return fmt.Errorf("for resource '%s': target app_name is missing", resource.Name)
+	} else {
+		errStrs := validation.IsDNS1123Label(target.AppName)
+
+		if len(errStrs) > 0 {
+			str := fmt.Sprintf("for resource '%s': invalid characters found in app_name:", resource.Name)
+			for _, errStr := range errStrs {
+				str += fmt.Sprintf("\n  * %s", errStr)
+			}
+
+			return fmt.Errorf("%s", str)
+		}
+	}
+
+	driverConfig := &BuildDriverConfig{}
+
+	err = mapstructure.Decode(resource.Config, driverConfig)
+
+	if err != nil {
+		return fmt.Errorf("for resource '%s': error parsing config: %w", resource.Name, err)
+	}
+
+	if driverConfig.Build.Method == "" {
+		return fmt.Errorf("for resource '%s': build method cannot be empty", resource.Name)
+	} else if driverConfig.Build.Method != "docker" &&
+		driverConfig.Build.Method != "pack" &&
+		driverConfig.Build.Method != "registry" {
+		return fmt.Errorf("for resource '%s': build method must be one of 'docker', 'pack', or 'registry'", resource.Name)
+	}
+
+	if driverConfig.Build.Method == "registry" {
+		if driverConfig.Build.Image == "" {
+			return fmt.Errorf("for resource '%s': image cannot be empty when using the 'registry' build method",
+				resource.Name)
+		} else if !strings.Contains(driverConfig.Build.Image, "{") {
+			if len(strings.Split(driverConfig.Build.Image, ":")) != 2 {
+				return fmt.Errorf("for resource '%s': image must be in the format 'image:tag'", resource.Name)
+			}
+
+			// check for valid image
+			_, err := reference.ParseNamed(driverConfig.Build.Image)
+
+			if err != nil {
+				return fmt.Errorf("for resource '%s': error parsing image: %w", resource.Name, err)
+			}
+		}
+	}
+
+	for _, eg := range driverConfig.EnvGroups {
+		if errStrs := validation.IsDNS1123Label(eg.Name); len(errStrs) > 0 {
+			str := fmt.Sprintf("for resource '%s': invalid characters found in env group '%s' name:",
+				resource.Name, eg.Name)
+			for _, errStr := range errStrs {
+				str += fmt.Sprintf("\n  * %s", errStr)
+			}
+
+			return fmt.Errorf("%s", str)
+		}
+	}
+
+	return nil
+}
+
+func pushImageDriverValidator(resource *types.Resource) error {
+	_, target, err := commonValidator(resource)
+
+	if err != nil {
+		return err
+	}
+
+	if target.AppName == "" {
+		return fmt.Errorf("for resource '%s': target app_name is missing", resource.Name)
+	} else {
+		errStrs := validation.IsDNS1123Label(target.AppName)
+
+		if len(errStrs) > 0 {
+			str := fmt.Sprintf("for resource '%s': invalid characters found in app_name:", resource.Name)
+			for _, errStr := range errStrs {
+				str += fmt.Sprintf("\n  * %s", errStr)
+			}
+
+			return fmt.Errorf("%s", str)
+		}
+	}
+
+	driverConfig := &PushDriverConfig{}
+
+	err = mapstructure.Decode(resource.Config, driverConfig)
+
+	if err != nil {
+		return fmt.Errorf("for resource '%s': error parsing config: %w", resource.Name, err)
+	}
+
+	if driverConfig.Push.Image == "" {
+		return fmt.Errorf("for resource '%s': image cannot be empty", resource.Name)
+	} else if !strings.Contains(driverConfig.Push.Image, "{") {
+		if len(strings.Split(driverConfig.Push.Image, ":")) != 2 {
+			return fmt.Errorf("for resource '%s': image must be in the format 'image:tag'", resource.Name)
+		}
+
+		// check for valid image
+		_, err := reference.ParseNamed(driverConfig.Push.Image)
+
+		if err != nil {
+			return fmt.Errorf("for resource '%s': error parsing image: %w", resource.Name, err)
+		}
+	}
+
+	return nil
+}
+
+func updateConfigDriverValidator(resource *types.Resource) error {
+	source, target, err := commonValidator(resource)
+
+	if err != nil {
+		return err
+	}
+
+	if target.AppName == "" {
+		return fmt.Errorf("for resource '%s': target app_name is missing", resource.Name)
+	} else {
+		errStrs := validation.IsDNS1123Label(target.AppName)
+
+		if len(errStrs) > 0 {
+			str := fmt.Sprintf("for resource '%s': invalid characters found in app_name:", resource.Name)
+			for _, errStr := range errStrs {
+				str += fmt.Sprintf("\n  * %s", errStr)
+			}
+
+			return fmt.Errorf("%s", str)
+		}
+	}
+
+	if source.Repo == "" {
+		if source.Name == "web" || source.Name == "worker" || source.Name == "job" {
+			source.Repo = "https://charts.getporter.dev"
+		} else {
+			source.Repo = "https://chart-addons.getporter.dev"
+		}
+	}
+
+	driverConfig := &UpdateConfigDriverConfig{}
+
+	err = mapstructure.Decode(resource.Config, driverConfig)
+
+	if err != nil {
+		return fmt.Errorf("for resource '%s': error parsing config: %w", resource.Name, err)
+	}
+
+	if driverConfig.UpdateConfig.Image == "" {
+		return fmt.Errorf("for resource '%s': image cannot be empty", resource.Name)
+	}
+
+	for _, eg := range driverConfig.EnvGroups {
+		if errStrs := validation.IsDNS1123Label(eg.Name); len(errStrs) > 0 {
+			str := fmt.Sprintf("for resource '%s': invalid characters found in env group '%s' name:",
+				resource.Name, eg.Name)
+			for _, errStr := range errStrs {
+				str += fmt.Sprintf("\n  * %s", errStr)
+			}
+
+			return fmt.Errorf("%s", str)
+		}
+	}
+
+	if len(driverConfig.Values) > 0 && source.Repo == "https://charts.getporter.dev" {
+		if source.Name == "web" {
+			err := validateWebChartValues(driverConfig.Values)
+
+			if err != nil {
+				return fmt.Errorf("for resource '%s': error validating values for web deployment: %w",
+					resource.Name, err)
+			}
+		} else if source.Name == "worker" {
+			err := validateWorkerChartValues(driverConfig.Values)
+
+			if err != nil {
+				return fmt.Errorf("for resource '%s': error validating values for worker deployment: %w",
+					resource.Name, err)
+			}
+		} else if source.Name == "job" {
+			err := validateJobChartValues(driverConfig.Values)
+
+			if err != nil {
+				return fmt.Errorf("for resource '%s': error validating values for job deployment: %w",
+					resource.Name, err)
+			}
+		}
+	}
+
+	return nil
+}
+
+func randomStringDriverValidator(resource *types.Resource) error {
+	driverConfig := &RandomStringDriverConfig{}
+
+	err := mapstructure.Decode(resource.Config, driverConfig)
+
+	if err != nil {
+		return fmt.Errorf("for resource '%s': error parsing config: %w", resource.Name, err)
+	}
+
+	return nil
+}
+
+func envGroupDriverValidator(resource *types.Resource) error {
+	target := &Target{}
+
+	err := mapstructure.Decode(resource.Target, target)
+
+	if err != nil {
+		return fmt.Errorf("for resource '%s': error parsing target: %w", resource.Name, err)
+	}
+
+	config := &EnvGroupDriverConfig{}
+
+	err = mapstructure.Decode(resource.Config, config)
+
+	if err != nil {
+		return fmt.Errorf("for resource '%s': error parsing config: %w", resource.Name, err)
+	}
+
+	for _, eg := range config.EnvGroups {
+		if errStrs := validation.IsDNS1123Label(eg.Name); len(errStrs) > 0 {
+			str := fmt.Sprintf("for resource '%s': invalid characters found in env group '%s' name:",
+				resource.Name, eg.Name)
+			for _, errStr := range errStrs {
+				str += fmt.Sprintf("\n  * %s", errStr)
+			}
+
+			return fmt.Errorf("%s", str)
+		}
+	}
+
+	return nil
+}
+
+func osEnvDriverValidator(resource *types.Resource) error {
+	return nil
+}

+ 105 - 0
internal/integrations/preview/schema_validate.go

@@ -0,0 +1,105 @@
+package preview
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/santhosh-tekuri/jsonschema/v5"
+	_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"
+)
+
+func validateWebChartValues(values map[string]interface{}) error {
+	compiler := jsonschema.NewCompiler()
+
+	scm, err := compiler.Compile("https://raw.githubusercontent.com/porter-dev/porter-charts/master/applications/web/validate.json")
+
+	if err != nil {
+		return fmt.Errorf("error compiling job chart values schema: %w", err)
+	}
+
+	jsonBytes, err := json.Marshal(values)
+
+	if err != nil {
+		return fmt.Errorf("error marshalling values to JSON: %w", err)
+	}
+
+	var v interface{}
+
+	if err := json.Unmarshal(jsonBytes, &v); err != nil {
+		return fmt.Errorf("error unmarshalling values JSON to interface: %w", err)
+	}
+
+	return scm.Validate(v)
+}
+
+func validateWorkerChartValues(values map[string]interface{}) error {
+	compiler := jsonschema.NewCompiler()
+
+	scm, err := compiler.Compile("https://raw.githubusercontent.com/porter-dev/porter-charts/master/applications/worker/validate.json")
+
+	if err != nil {
+		return fmt.Errorf("error compiling job chart values schema: %w", err)
+	}
+
+	jsonBytes, err := json.Marshal(values)
+
+	if err != nil {
+		return fmt.Errorf("error marshalling values to JSON: %w", err)
+	}
+
+	var v interface{}
+
+	if err := json.Unmarshal(jsonBytes, &v); err != nil {
+		return fmt.Errorf("error unmarshalling values JSON to interface: %w", err)
+	}
+
+	return scm.Validate(v)
+}
+
+func validateJobChartValues(values map[string]interface{}) error {
+	compiler := jsonschema.NewCompiler()
+
+	scm, err := compiler.Compile("https://raw.githubusercontent.com/porter-dev/porter-charts/master/applications/job/validate.json")
+
+	if err != nil {
+		return fmt.Errorf("error compiling job chart values schema: %w", err)
+	}
+
+	jsonBytes, err := json.Marshal(values)
+
+	if err != nil {
+		return fmt.Errorf("error marshalling values to JSON: %w", err)
+	}
+
+	var v interface{}
+
+	if err := json.Unmarshal(jsonBytes, &v); err != nil {
+		return fmt.Errorf("error unmarshalling values JSON to interface: %w", err)
+	}
+
+	return scm.Validate(v)
+}
+
+func validatePostgresChartValues(values map[string]interface{}) error {
+	compiler := jsonschema.NewCompiler()
+
+	scm, err := compiler.Compile("https://raw.githubusercontent.com/porter-dev/porter-charts/master/addons/postgresql/values.schema.json")
+
+	if err != nil {
+		return fmt.Errorf("error compiling postgres chart values schema: %w", err)
+	}
+
+	jsonBytes, err := json.Marshal(values)
+
+	if err != nil {
+		return fmt.Errorf("error marshalling values to JSON: %w", err)
+	}
+
+	var v interface{}
+
+	if err := json.Unmarshal(jsonBytes, &v); err != nil {
+		return fmt.Errorf("error unmarshalling values JSON to interface: %w", err)
+	}
+
+	return scm.Validate(v)
+}

+ 91 - 0
internal/integrations/preview/utils.go

@@ -0,0 +1,91 @@
+package preview
+
+import "github.com/porter-dev/porter/api/types"
+
+type Source struct {
+	Name          string
+	Repo          string
+	Version       string
+	IsApplication bool
+	SourceValues  map[string]interface{}
+}
+
+type Target struct {
+	AppName   string `mapstructure:"app_name"`
+	Project   uint
+	Cluster   uint
+	Namespace string
+}
+
+type RandomStringDriverConfig struct {
+	Length uint
+	Lower  bool
+}
+
+type EnvGroupDriverConfig struct {
+	EnvGroups []*types.EnvGroup `mapstructure:"env_groups"`
+}
+
+type UpdateConfigDriverConfig struct {
+	WaitForJob bool
+
+	// If set to true, this does not run an update, it only creates the initial application and job,
+	// skipping subsequent updates
+	OnlyCreate bool
+
+	UpdateConfig struct {
+		Image string
+		Tag   string
+	} `mapstructure:"update_config"`
+
+	EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
+
+	Values map[string]interface{}
+}
+
+type PushDriverConfig struct {
+	Push struct {
+		UsePackCache bool `mapstructure:"use_pack_cache"`
+		Image        string
+	}
+}
+
+type BuildDriverConfig struct {
+	Build struct {
+		UsePackCache bool `mapstructure:"use_pack_cache"`
+		Method       string
+		Context      string
+		Dockerfile   string
+		Builder      string
+		Buildpacks   []string
+		Image        string
+		Env          map[string]string
+	}
+
+	EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
+
+	Values map[string]interface{}
+}
+
+type ApplicationConfig struct {
+	WaitForJob bool
+
+	// If set to true, this does not run an update, it only creates the initial application and job,
+	// skipping subsequent updates
+	OnlyCreate bool
+
+	Build struct {
+		UseCache   bool `mapstructure:"use_cache"`
+		Method     string
+		Context    string
+		Dockerfile string
+		Image      string
+		Builder    string
+		Buildpacks []string
+		Env        map[string]string
+	}
+
+	EnvGroups []types.EnvGroupMeta `mapstructure:"env_groups"`
+
+	Values map[string]interface{}
+}

+ 75 - 0
internal/integrations/preview/validate.go

@@ -0,0 +1,75 @@
+package preview
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/porter-dev/switchboard/pkg/parser"
+	"github.com/porter-dev/switchboard/pkg/types"
+	"k8s.io/apimachinery/pkg/util/validation"
+)
+
+var (
+	ErrNoPorterYAMLFile    = errors.New("porter.yaml does not exist in the root of this repository")
+	ErrEmptyPorterYAMLFile = errors.New("porter.yaml is empty")
+
+	ErrUnsupportedDriver = errors.New("no such driver")
+)
+
+type driverBasedResourceValidator func(*types.Resource) error
+
+var driverValidators = make(map[string]driverBasedResourceValidator)
+
+func init() {
+	driverValidators[""] = deployDriverValidator
+	driverValidators["deploy"] = deployDriverValidator
+	driverValidators["build-image"] = buildImageDriverValidator
+	driverValidators["push-image"] = pushImageDriverValidator
+	driverValidators["update-config"] = updateConfigDriverValidator
+	driverValidators["random-string"] = randomStringDriverValidator
+	driverValidators["env-group"] = envGroupDriverValidator
+	driverValidators["os-env"] = osEnvDriverValidator
+}
+
+func Validate(contents string) []error {
+	var errors []error
+
+	resGroup, err := parser.ParseRawBytes([]byte(contents))
+
+	if err != nil {
+		errors = append(errors, fmt.Errorf("error parsing porter.yaml: %w", err))
+		return errors
+	}
+
+	depResolver := newDependencyResolver(resGroup.Resources)
+
+	err = depResolver.Resolve()
+
+	if err != nil {
+		errors = append(errors, fmt.Errorf("error resolving dependencies: %w", err))
+		return errors
+	}
+
+	for _, res := range resGroup.Resources {
+		if len(res.Name) == 0 {
+			errors = append(errors, fmt.Errorf("resource has no name"))
+		} else if errStrs := validation.IsDNS1123Label(res.Name); len(errStrs) > 0 {
+			str := fmt.Sprintf("for resource '%s': invalid characters found in name:", res.Name)
+			for _, errStr := range errStrs {
+				str += fmt.Sprintf("\n  * %s", errStr)
+			}
+
+			errors = append(errors, fmt.Errorf("%s", str))
+		}
+
+		if validator, ok := driverValidators[res.Driver]; ok {
+			if err := validator(res); err != nil {
+				errors = append(errors, err)
+			}
+		} else {
+			errors = append(errors, fmt.Errorf("for resource '%s': %w: %s", res.Name, ErrUnsupportedDriver, res.Driver))
+		}
+	}
+
+	return errors
+}