Просмотр исходного кода

Merge branch 'nafees/pr-env-validator' into dev

Mohammed Nafees 3 лет назад
Родитель
Сommit
700db96877

+ 48 - 1
cli/cmd/apply.go

@@ -72,16 +72,41 @@ 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).Printf("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 {
+	err := applyValidate()
+
+	if err != nil {
+		return err
+	}
+
 	fileBytes, err := ioutil.ReadFile(porterYAML)
 
 	if err != nil {
@@ -135,6 +160,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

+ 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=

+ 13 - 11
internal/integrations/preview/dep_resolver.go

@@ -23,20 +23,22 @@ func newDependencyResolver(resources []*types.Resource) *dependencyResolver {
 }
 
 func (r *dependencyResolver) Resolve() error {
-	// 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)
-		}
+	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...)
-	}
+			r.graph[resource.Name] = append(r.graph[resource.Name], resource.DependsOn...)
+		}
 
-	err := r.depResolve(r.resources[0].Name)
+		err := r.depResolve(r.resources[0].Name)
 
-	if err != nil {
-		return err
+		if err != nil {
+			return err
+		}
 	}
 
 	return nil

+ 156 - 1
internal/integrations/preview/driver_validators.go

@@ -5,6 +5,7 @@ import (
 
 	"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) {
@@ -34,7 +35,15 @@ func deployDriverValidator(resource *types.Resource) error {
 		return err
 	}
 
-	if source.Repo == "" || source.Repo == "https://charts.getporter.dev" {
+	if source.Name == "" {
+		return fmt.Errorf("for resource '%s': source name cannot be empty", resource.Name)
+	}
+
+	if source.Repo == "" {
+		source.Repo = "https://charts.getporter.dev"
+	}
+
+	if source.Repo == "https://charts.getporter.dev" {
 		appConfig := &ApplicationConfig{}
 
 		err = mapstructure.Decode(resource.Config, appConfig)
@@ -42,6 +51,59 @@ func deployDriverValidator(resource *types.Resource) error {
 		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 == "docker" && appConfig.Build.Dockerfile == "" {
+			return fmt.Errorf("for resource '%s': dockerfile cannot be empty when using the 'docker' build method",
+				resource.Name)
+		} else if appConfig.Build.Method == "registry" && appConfig.Build.Image == "" {
+			return fmt.Errorf("for resource '%s': image cannot be empty when using the 'registry' build method",
+				resource.Name)
+		}
+
+		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)
+				}
+			}
+		}
 	}
 
 	return nil
@@ -56,6 +118,17 @@ func buildImageDriverValidator(resource *types.Resource) error {
 
 	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{}
@@ -66,6 +139,34 @@ func buildImageDriverValidator(resource *types.Resource) error {
 		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 == "docker" && driverConfig.Build.Dockerfile == "" {
+		return fmt.Errorf("for resource '%s': dockerfile cannot be empty when using the 'docker' build method",
+			resource.Name)
+	} else if driverConfig.Build.Method == "registry" && driverConfig.Build.Image == "" {
+		return fmt.Errorf("for resource '%s': image cannot be empty when using the 'registry' build method",
+			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)
+		}
+	}
+
 	return nil
 }
 
@@ -78,6 +179,17 @@ func pushImageDriverValidator(resource *types.Resource) error {
 
 	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{}
@@ -88,6 +200,10 @@ func pushImageDriverValidator(resource *types.Resource) error {
 		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)
+	}
+
 	return nil
 }
 
@@ -100,6 +216,17 @@ func updateConfigDriverValidator(resource *types.Resource) error {
 
 	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 := &UpdateConfigDriverConfig{}
@@ -110,6 +237,22 @@ func updateConfigDriverValidator(resource *types.Resource) error {
 		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)
+		}
+	}
+
 	return nil
 }
 
@@ -142,6 +285,18 @@ func envGroupDriverValidator(resource *types.Resource) error {
 		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
 }
 

+ 201 - 0
internal/integrations/preview/embed/deploy_driver.schema.json.unused

@@ -0,0 +1,201 @@
+{
+  "$schema": "http://json-schema.org/schema#",
+  "title": "schema for the default deploy driver",
+  "type": "object",
+  "properties": {
+    "name": {
+      "type": "string",
+      "description": "resource name",
+      "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
+      "maxLength": 50
+    },
+    "driver": {
+      "type": "string",
+      "description": "resource driver",
+      "enum": ["deploy", ""]
+    },
+    "depends_on": {
+      "type": "array",
+      "description": "list of resource names this resource depends on",
+      "minItems": 1,
+      "items": {
+        "type": "string",
+        "description": "dependency resource name",
+        "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
+        "maxLength": 50
+      }
+    },
+    "source": {
+      "type": "object",
+      "description": "resource source",
+      "properties": {
+        "name": {
+          "type": "string",
+          "description": "source Helm chart name"
+        },
+        "version": {
+          "type": "string",
+          "description": "source Helm chart version"
+        },
+        "repo": {
+          "type": "string",
+          "description": "source Helm chart repo URL",
+          "default": "https://charts.getporter.dev"
+        }
+      },
+      "required": ["name"]
+    },
+    "target": {
+      "type": "object",
+      "description": "resource target",
+      "properties": {
+        "project": {
+          "type": "integer",
+          "description": "target Porter project ID"
+        },
+        "cluster": {
+          "type": "integer",
+          "description": "target Porter cluster ID"
+        },
+        "namespace": {
+          "type": "string",
+          "description": "target namespace"
+        }
+      }
+    },
+    "config": {
+      "type": "object",
+      "description": "resource configuration",
+      "additionalProperties": true
+    },
+    "if": {
+      "properties": {
+        "source": {
+          "properties": { "repo": { "const": "https://charts.getporter.dev" } }
+        }
+      }
+    },
+    "then": {
+      "properties": {
+        "config": {
+          "properties": {
+            "waitForJob": {
+              "type": "boolean",
+              "description": "wait for job to complete"
+            },
+            "onlyCreate": {
+              "type": "boolean",
+              "description": "only create the resource"
+            },
+            "build": {
+              "type": "object",
+              "description": "build configuration",
+              "properties": {
+                "use_cache": {
+                  "type": "boolean",
+                  "description": "use Porter build cache"
+                },
+                "method": {
+                  "type": "string",
+                  "description": "build method",
+                  "default": "docker",
+                  "enum": ["docker", "pack", "registry"]
+                },
+                "context": {
+                  "type": "string",
+                  "description": "build context"
+                },
+                "dockerfile": {
+                  "type": "string",
+                  "description": "Dockerfile path"
+                },
+                "image": {
+                  "type": "string",
+                  "description": "image name"
+                },
+                "builder": {
+                  "type": "string",
+                  "description": "buildpacks builder image"
+                },
+                "buildpacks": {
+                  "type": "array",
+                  "description": "list of buildpacks",
+                  "minItems": 1,
+                  "items": {
+                    "type": "string",
+                    "description": "buildpack"
+                  }
+                },
+                "env": {
+                  "type": "object",
+                  "description": "build-time environment variables",
+                  "additionalProperties": { "type": "string" }
+                }
+              },
+              "allOf": [
+                {
+                  "if": {
+                    "properties": {
+                      "method": { "const": "docker" }
+                    }
+                  },
+                  "then": {
+                    "dependentRequired": {
+                      "method": ["dockerfile"]
+                    }
+                  }
+                },
+                {
+                  "if": {
+                    "properties": {
+                      "method": { "const": "registry" }
+                    }
+                  },
+                  "then": {
+                    "dependentRequired": {
+                      "method": ["image"]
+                    }
+                  }
+                }
+              ]
+            },
+            "env_groups": {
+              "type": "array",
+              "description": "list of environment groups to use in the deployment",
+              "minItems": 1,
+              "items": {
+                "type": "object",
+                "description": "environment group",
+                "properties": {
+                  "name": {
+                    "type": "string",
+                    "description": "environment group name"
+                  },
+                  "version": {
+                    "type": "integer",
+                    "minimum": 0,
+                    "default": 0,
+                    "description": "environment group version"
+                  },
+                  "namespace": {
+                    "type": "string",
+                    "description": "environment group namespace"
+                  }
+                },
+                "required": ["name"]
+              }
+            },
+            "values": {
+              "type": "object",
+              "description": "Helm values to use for the deployment",
+              "additionalProperties": true
+            }
+          },
+          "required": ["build"]
+        }
+      },
+      "required": ["config"]
+    }
+  },
+  "required": ["name", "source"]
+}

+ 73 - 0
internal/integrations/preview/embed/job.values.schema.json

@@ -0,0 +1,73 @@
+{
+  "$schema": "http://json-schema.org/schema#",
+  "type": "object",
+  "properties": {
+    "replicaCount": {
+      "type": "integer",
+      "minimum": 1,
+      "default": 1
+    },
+    "container": {
+      "type": "object",
+      "properties": {
+        "port": {
+          "type": "integer",
+          "default": 80
+        },
+        "command": {
+          "type": "string"
+        },
+        "env": {
+          "type": "object",
+          "properties": {
+            "normal": {
+              "type": "object",
+              "additionalProperties": {
+                "type": "string"
+              }
+            }
+          }
+        }
+      }
+    },
+    "schedule": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean",
+          "default": false
+        },
+        "value": {
+          "type": "string",
+          "default": "*/5 * * * *"
+        },
+        "successfulHistory": {
+          "type": "integer",
+          "default": 20
+        },
+        "failedHistory": {
+          "type": "integer",
+          "default": 20
+        }
+      }
+    },
+    "resources": {
+      "type": "object",
+      "properties": {
+        "requests": {
+          "type": "object",
+          "properties": {
+            "cpu": {
+              "type": "string",
+              "pattern": "^\\d+(m){0,1}$"
+            },
+            "memory": {
+              "type": "string",
+              "pattern": "^\\d+(Ki|Mi|Gi)$"
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 90 - 0
internal/integrations/preview/embed/porteryaml.schema.json.unused

@@ -0,0 +1,90 @@
+{
+  "$schema": "http://json-schema.org/schema#",
+  "type": "object",
+  "properties": {
+    "version": {
+      "type": "string",
+      "description": "porter.yaml version",
+      "pattern": "^v[1-9][0-9]*$"
+    },
+    "resources": {
+      "type": "array",
+      "description": "list of resources",
+      "minItems": 1,
+      "items": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string",
+            "description": "resource name",
+            "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
+            "maxLength": 50
+          },
+          "driver": {
+            "type": "string",
+            "description": "resource driver"
+          },
+          "depends_on": {
+            "type": "array",
+            "description": "list of resource names this resource depends on",
+            "minItems": 1,
+            "items": {
+              "type": "string",
+              "description": "dependency resource name",
+              "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
+              "maxLength": 50
+            }
+          },
+          "source": {
+            "type": "object",
+            "description": "resource source",
+            "properties": {
+              "name": {
+                "type": "string",
+                "description": "source Helm chart name"
+              },
+              "version": {
+                "type": "string",
+                "description": "source Helm chart version"
+              },
+              "repo": {
+                "type": "string",
+                "description": "source Helm chart repo URL"
+              }
+            }
+          },
+          "target": {
+            "type": "object",
+            "description": "resource target",
+            "properties": {
+              "project": {
+                "type": "integer",
+                "description": "target Porter project ID"
+              },
+              "cluster": {
+                "type": "integer",
+                "description": "target Porter cluster ID"
+              },
+              "namespace": {
+                "type": "string",
+                "description": "target namespace"
+              },
+              "app_name": {
+                "type": "string",
+                "description": "target app name",
+                "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
+                "maxLength": 50
+              }
+            }
+          },
+          "config": {
+            "type": "object",
+            "description": "resource config"
+          }
+        },
+        "required": ["name"]
+      }
+    }
+  },
+  "required": ["version", "resources"]
+}

+ 289 - 0
internal/integrations/preview/embed/web.values.schema.json

@@ -0,0 +1,289 @@
+{
+  "$schema": "http://json-schema.org/schema#",
+  "type": "object",
+  "properties": {
+    "replicaCount": {
+      "type": "integer",
+      "minimum": 1,
+      "default": 1
+    },
+    "ingress": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean",
+          "default": true
+        },
+        "hosts": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "porter_hosts": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "provider": {
+          "type": "string"
+        },
+        "custom_domain": {
+          "type": "boolean",
+          "default": false
+        },
+        "custom_paths": {
+          "type": "string"
+        },
+        "rewriteCustomPathsEnabled": {
+          "type": "boolean",
+          "default": true
+        },
+        "annotations": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "wildcard": {
+          "type": "boolean",
+          "default": false
+        },
+        "tls": {
+          "type": "boolean",
+          "default": true
+        },
+        "useDefaultIngressTLSSecret": {
+          "type": "boolean",
+          "default": false
+        }
+      }
+    },
+    "container": {
+      "type": "object",
+      "properties": {
+        "port": {
+          "type": "integer",
+          "default": 80
+        },
+        "command": {
+          "type": "string"
+        },
+        "args": {
+          "type": "array",
+          "items": {
+            "type": "string"
+          }
+        },
+        "env": {
+          "type": "object",
+          "properties": {
+            "normal": {
+              "type": "object",
+              "additionalProperties": {
+                "type": "string"
+              }
+            }
+          }
+        }
+      }
+    },
+    "resources": {
+      "type": "object",
+      "properties": {
+        "requests": {
+          "type": "object",
+          "properties": {
+            "cpu": {
+              "type": "string",
+              "pattern": "^\\d+(m){0,1}$"
+            },
+            "memory": {
+              "type": "string",
+              "pattern": "^\\d+(Ki|Mi|Gi)$"
+            }
+          }
+        }
+      }
+    },
+    "autoscaling": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean",
+          "default": false
+        },
+        "minReplicas": {
+          "type": "integer",
+          "default": 1
+        },
+        "maxReplicas": {
+          "type": "integer",
+          "default": 10
+        },
+        "targetCPUUtilizationPercentage": {
+          "type": "integer",
+          "default": 50
+        },
+        "targetMemoryUtilizationPercentage": {
+          "type": "integer",
+          "default": 50
+        }
+      }
+    },
+    "health": {
+      "type": "object",
+      "properties": {
+        "livenessProbe": {
+          "type": "object",
+          "properties": {
+            "enabled": {
+              "type": "boolean",
+              "default": false
+            },
+            "path": {
+              "type": "string",
+              "default": "/livez"
+            },
+            "scheme": {
+              "type": "string",
+              "default": "HTTP"
+            },
+            "initialDelaySeconds": {
+              "type": "integer",
+              "default": 0
+            },
+            "periodSeconds": {
+              "type": "integer",
+              "default": 5
+            },
+            "timeoutSeconds": {
+              "type": "integer",
+              "default": 1
+            },
+            "successThreshold": {
+              "type": "integer",
+              "default": 1
+            },
+            "failureThreshold": {
+              "type": "integer",
+              "default": 3
+            },
+            "auth": {
+              "type": "object",
+              "properties": {
+                "enabled": {
+                  "type": "boolean",
+                  "default": false
+                },
+                "username": {
+                  "type": "string"
+                },
+                "password": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        },
+        "readinessProbe": {
+          "type": "object",
+          "properties": {
+            "enabled": {
+              "type": "boolean",
+              "default": false
+            },
+            "path": {
+              "type": "string",
+              "default": "/readyz"
+            },
+            "scheme": {
+              "type": "string",
+              "default": "HTTP"
+            },
+            "initialDelaySeconds": {
+              "type": "integer",
+              "default": 0
+            },
+            "periodSeconds": {
+              "type": "integer",
+              "default": 5
+            },
+            "timeoutSeconds": {
+              "type": "integer",
+              "default": 1
+            },
+            "successThreshold": {
+              "type": "integer",
+              "default": 1
+            },
+            "failureThreshold": {
+              "type": "integer",
+              "default": 3
+            },
+            "auth": {
+              "type": "object",
+              "properties": {
+                "enabled": {
+                  "type": "boolean",
+                  "default": false
+                },
+                "username": {
+                  "type": "string"
+                },
+                "password": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        },
+        "startupProbe": {
+          "type": "object",
+          "properties": {
+            "enabled": {
+              "type": "boolean",
+              "default": false
+            },
+            "path": {
+              "type": "string",
+              "default": "/startupz"
+            },
+            "scheme": {
+              "type": "string",
+              "default": "HTTP"
+            },
+            "failureThreshold": {
+              "type": "integer",
+              "default": 3
+            },
+            "periodSeconds": {
+              "type": "integer",
+              "default": 5
+            },
+            "timeoutSeconds": {
+              "type": "integer",
+              "default": 1
+            },
+            "auth": {
+              "type": "object",
+              "properties": {
+                "enabled": {
+                  "type": "boolean",
+                  "default": false
+                },
+                "username": {
+                  "type": "string"
+                },
+                "password": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 136 - 0
internal/integrations/preview/embed/worker.values.schema.json

@@ -0,0 +1,136 @@
+{
+  "$schema": "http://json-schema.org/schema#",
+  "type": "object",
+  "properties": {
+    "replicaCount": {
+      "type": "integer",
+      "minimum": 1,
+      "default": 1
+    },
+    "container": {
+      "type": "object",
+      "properties": {
+        "port": {
+          "type": "integer",
+          "default": 80
+        },
+        "command": {
+          "type": "string"
+        },
+        "env": {
+          "type": "object",
+          "properties": {
+            "normal": {
+              "type": "object",
+              "additionalProperties": {
+                "type": "string"
+              }
+            }
+          }
+        }
+      }
+    },
+    "resources": {
+      "type": "object",
+      "properties": {
+        "requests": {
+          "type": "object",
+          "properties": {
+            "cpu": {
+              "type": "string",
+              "pattern": "^\\d+(m){0,1}$"
+            },
+            "memory": {
+              "type": "string",
+              "pattern": "^\\d+(Ki|Mi|Gi)$"
+            }
+          }
+        }
+      }
+    },
+    "autoscaling": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean",
+          "default": false
+        },
+        "minReplicas": {
+          "type": "integer",
+          "default": 1
+        },
+        "maxReplicas": {
+          "type": "integer",
+          "default": 10
+        },
+        "targetCPUUtilizationPercentage": {
+          "type": "integer",
+          "default": 50
+        },
+        "targetMemoryUtilizationPercentage": {
+          "type": "integer",
+          "default": 50
+        }
+      }
+    },
+    "health": {
+      "type": "object",
+      "properties": {
+        "enabled": {
+          "type": "boolean",
+          "default": false
+        },
+        "command": {
+          "type": "string",
+          "default": "ls -l"
+        },
+        "periodSeconds": {
+          "type": "integer",
+          "default": 5
+        },
+        "failureThreshold": {
+          "type": "integer",
+          "default": 3
+        },
+        "readinessProbe": {
+          "type": "object",
+          "properties": {
+            "enabled": {
+              "type": "boolean",
+              "default": false
+            },
+            "command": {
+              "type": "string",
+              "default": "ls -l"
+            },
+            "periodSeconds": {
+              "type": "integer",
+              "default": 5
+            }
+          }
+        },
+        "startupProbe": {
+          "type": "object",
+          "properties": {
+            "enabled": {
+              "type": "boolean",
+              "default": false
+            },
+            "command": {
+              "type": "string",
+              "default": "ls -l"
+            },
+            "failureThreshold": {
+              "type": "integer",
+              "default": 3
+            },
+            "periodSeconds": {
+              "type": "integer",
+              "default": 5
+            }
+          }
+        }
+      }
+    }
+  }
+}

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

@@ -0,0 +1,92 @@
+package preview
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/santhosh-tekuri/jsonschema/v5"
+)
+
+func validateWebChartValues(values map[string]interface{}) error {
+	webValuesSchema, err := schemas.ReadFile("embed/web.values.schema.json")
+
+	if err != nil {
+		return fmt.Errorf("error reading web chart values schema: %w", err)
+	}
+
+	scm, err := jsonschema.CompileString("web.values.schema.json", string(webValuesSchema))
+
+	if err != nil {
+		return fmt.Errorf("error compiling web 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 {
+	workerValuesSchema, err := schemas.ReadFile("embed/worker.values.schema.json")
+
+	if err != nil {
+		return fmt.Errorf("error reading worker chart values schema: %w", err)
+	}
+
+	scm, err := jsonschema.CompileString("worker.values.schema.json", string(workerValuesSchema))
+
+	if err != nil {
+		return fmt.Errorf("error compiling worker 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 {
+	jobValuesSchema, err := schemas.ReadFile("embed/job.values.schema.json")
+
+	if err != nil {
+		return fmt.Errorf("error reading job chart values schema: %w", err)
+	}
+
+	scm, err := jsonschema.CompileString("job.values.schema.json", string(jobValuesSchema))
+
+	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)
+}

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

@@ -1,13 +1,18 @@
 package preview
 
 import (
+	"embed"
 	"errors"
 	"fmt"
 
 	"github.com/porter-dev/switchboard/pkg/parser"
 	"github.com/porter-dev/switchboard/pkg/types"
+	"k8s.io/apimachinery/pkg/util/validation"
 )
 
+//go:embed embed/*.schema.json
+var schemas embed.FS
+
 var (
 	ErrNoPorterYAMLFile    = errors.New("porter.yaml does not exist in the root of this repository")
 	ErrEmptyPorterYAMLFile = errors.New("porter.yaml is empty")
@@ -50,6 +55,15 @@ func Validate(contents string) []error {
 	}
 
 	for _, res := range resGroup.Resources {
+		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)