Explorar el Código

use json schema validator

Mohammed Nafees hace 3 años
padre
commit
80a7cccb45

+ 2 - 0
cli/cmd/apply.go

@@ -83,6 +83,8 @@ var applyValidateCmd = &cobra.Command{
 		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")
 		}
 	},
 }

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

+ 16 - 14
internal/integrations/preview/driver_validators.go

@@ -1,10 +1,12 @@
 package preview
 
 import (
+	"encoding/json"
 	"fmt"
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/porter-dev/switchboard/pkg/types"
+	jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
 )
 
 func commonValidator(resource *types.Resource) (*Source, *Target, error) {
@@ -28,31 +30,31 @@ func commonValidator(resource *types.Resource) (*Source, *Target, error) {
 }
 
 func deployDriverValidator(resource *types.Resource) error {
-	source, _, err := commonValidator(resource)
+	deployDriverSchema, err := schemas.ReadFile("embed/deploy_driver.schema.json")
 
 	if err != nil {
-		return err
+		return fmt.Errorf("for resource '%s': error reading deploy driver schema: %w", resource.Name, err)
 	}
 
-	if source.Name == "" {
-		return fmt.Errorf("for resource '%s': source name is empty", resource.Name)
-	}
+	deployDriverSchemaCompiler, err := jsonschema.CompileString("deploy_driver.schema.json", string(deployDriverSchema))
 
-	if source.Repo == "" {
-		source.Repo = "https://charts.getporter.dev"
+	if err != nil {
+		return fmt.Errorf("for resource '%s': error compiling deploy driver schema: %w", resource.Name, err)
 	}
 
-	if source.Repo == "https://charts.getporter.dev" {
-		appConfig := &ApplicationConfig{}
+	jsonBytes, err := json.Marshal(resource)
 
-		err = mapstructure.Decode(resource.Config, appConfig)
+	if err != nil {
+		return fmt.Errorf("for resource '%s': error marshalling to JSON: %w", resource.Name, err)
+	}
+
+	var v interface{}
 
-		if err != nil {
-			return fmt.Errorf("for resource '%s': error parsing config: %w", resource.Name, err)
-		}
+	if err := json.Unmarshal(jsonBytes, &v); err != nil {
+		return fmt.Errorf("for resource '%s': error unmarshalling to interface: %w", resource.Name, err)
 	}
 
-	return nil
+	return deployDriverSchemaCompiler.Validate(v)
 }
 
 func buildImageDriverValidator(resource *types.Resource) error {

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

@@ -0,0 +1,196 @@
+{
+  "$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"
+        }
+      }
+    },
+    "if": {
+      "properties": {
+        "source": {
+          "properties": { "repo": { "const": "https://charts.getporter.dev" } }
+        }
+      }
+    },
+    "then": {
+      "properties": {
+        "config": {
+          "type": "object",
+          "description": "resource configuration",
+          "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": ["name"]
+}

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

@@ -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"]
+}

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

@@ -1,13 +1,20 @@
 package preview
 
 import (
+	"embed"
+	"encoding/json"
 	"errors"
 	"fmt"
 
 	"github.com/porter-dev/switchboard/pkg/parser"
 	"github.com/porter-dev/switchboard/pkg/types"
+	jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
+	"sigs.k8s.io/yaml"
 )
 
+//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")
@@ -49,6 +56,13 @@ func Validate(contents string) []error {
 		return errors
 	}
 
+	err = semanticCheck(contents)
+
+	if err != nil {
+		errors = append(errors, fmt.Errorf("error validating porter.yaml: %w", err))
+		return errors
+	}
+
 	for _, res := range resGroup.Resources {
 		if validator, ok := driverValidators[res.Driver]; ok {
 			if err := validator(res); err != nil {
@@ -61,3 +75,31 @@ func Validate(contents string) []error {
 
 	return errors
 }
+
+func semanticCheck(contents string) error {
+	porterYAMLSchema, err := schemas.ReadFile("embed/porteryaml.schema.json")
+
+	if err != nil {
+		return fmt.Errorf("error reading porterYAML schema: %w", err)
+	}
+
+	porterYAMLSchemaCompiler, err := jsonschema.CompileString("porteryaml.schema.json", string(porterYAMLSchema))
+
+	if err != nil {
+		return fmt.Errorf("error compiling porterYAML schema: %w", err)
+	}
+
+	jsonBytes, err := yaml.YAMLToJSON([]byte(contents))
+
+	if err != nil {
+		return fmt.Errorf("error converting porter.yaml to JSON: %w", err)
+	}
+
+	var v interface{}
+
+	if err := json.Unmarshal(jsonBytes, &v); err != nil {
+		return fmt.Errorf("error unmarshalling porter.yaml to interface: %w", err)
+	}
+
+	return porterYAMLSchemaCompiler.Validate(v)
+}