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

add web, worker, job schema validation

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

+ 67 - 15
internal/integrations/preview/driver_validators.go

@@ -1,12 +1,11 @@
 package preview
 package preview
 
 
 import (
 import (
-	"encoding/json"
 	"fmt"
 	"fmt"
 
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/mitchellh/mapstructure"
 	"github.com/porter-dev/switchboard/pkg/types"
 	"github.com/porter-dev/switchboard/pkg/types"
-	jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
+	"k8s.io/apimachinery/pkg/util/validation"
 )
 )
 
 
 func commonValidator(resource *types.Resource) (*Source, *Target, error) {
 func commonValidator(resource *types.Resource) (*Source, *Target, error) {
@@ -30,31 +29,84 @@ func commonValidator(resource *types.Resource) (*Source, *Target, error) {
 }
 }
 
 
 func deployDriverValidator(resource *types.Resource) error {
 func deployDriverValidator(resource *types.Resource) error {
-	deployDriverSchema, err := schemas.ReadFile("embed/deploy_driver.schema.json")
+	source, _, err := commonValidator(resource)
 
 
 	if err != nil {
 	if err != nil {
-		return fmt.Errorf("for resource '%s': error reading deploy driver schema: %w", resource.Name, err)
+		return err
 	}
 	}
 
 
-	deployDriverSchemaCompiler, err := jsonschema.CompileString("deploy_driver.schema.json", string(deployDriverSchema))
+	if source.Name == "" {
+		return fmt.Errorf("for resource '%s': source name cannot be empty", resource.Name)
+	}
 
 
-	if err != nil {
-		return fmt.Errorf("for resource '%s': error compiling deploy driver schema: %w", resource.Name, err)
+	if source.Repo == "" {
+		source.Repo = "https://charts.getporter.dev"
 	}
 	}
 
 
-	jsonBytes, err := json.Marshal(resource)
+	if source.Repo == "https://charts.getporter.dev" {
+		appConfig := &ApplicationConfig{}
 
 
-	if err != nil {
-		return fmt.Errorf("for resource '%s': error marshalling to JSON: %w", resource.Name, err)
-	}
+		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)
+		}
 
 
-	var v interface{}
+		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)
+		}
 
 
-	if err := json.Unmarshal(jsonBytes, &v); err != nil {
-		return fmt.Errorf("for resource '%s': error unmarshalling to interface: %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)
+				}
+			}
+		}
 	}
 	}
 
 
-	return deployDriverSchemaCompiler.Validate(v)
+	return nil
 }
 }
 
 
 func buildImageDriverValidator(resource *types.Resource) error {
 func buildImageDriverValidator(resource *types.Resource) error {

+ 10 - 5
internal/integrations/preview/embed/deploy_driver.schema.json

@@ -63,6 +63,11 @@
         }
         }
       }
       }
     },
     },
+    "config": {
+      "type": "object",
+      "description": "resource configuration",
+      "additionalProperties": true
+    },
     "if": {
     "if": {
       "properties": {
       "properties": {
         "source": {
         "source": {
@@ -73,8 +78,6 @@
     "then": {
     "then": {
       "properties": {
       "properties": {
         "config": {
         "config": {
-          "type": "object",
-          "description": "resource configuration",
           "properties": {
           "properties": {
             "waitForJob": {
             "waitForJob": {
               "type": "boolean",
               "type": "boolean",
@@ -187,10 +190,12 @@
               "description": "Helm values to use for the deployment",
               "description": "Helm values to use for the deployment",
               "additionalProperties": true
               "additionalProperties": true
             }
             }
-          }
+          },
+          "required": ["build"]
         }
         }
-      }
+      },
+      "required": ["config"]
     }
     }
   },
   },
-  "required": ["name"]
+  "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)$"
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 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)
+}

+ 9 - 37
internal/integrations/preview/validate.go

@@ -2,14 +2,12 @@ package preview
 
 
 import (
 import (
 	"embed"
 	"embed"
-	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 
 
 	"github.com/porter-dev/switchboard/pkg/parser"
 	"github.com/porter-dev/switchboard/pkg/parser"
 	"github.com/porter-dev/switchboard/pkg/types"
 	"github.com/porter-dev/switchboard/pkg/types"
-	jsonschema "github.com/santhosh-tekuri/jsonschema/v5"
-	"sigs.k8s.io/yaml"
+	"k8s.io/apimachinery/pkg/util/validation"
 )
 )
 
 
 //go:embed embed/*.schema.json
 //go:embed embed/*.schema.json
@@ -56,14 +54,16 @@ func Validate(contents string) []error {
 		return errors
 		return errors
 	}
 	}
 
 
-	err = semanticCheck(contents)
+	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)
+			}
 
 
-	if err != nil {
-		errors = append(errors, fmt.Errorf("error validating porter.yaml: %w", err))
-		return errors
-	}
+			errors = append(errors, fmt.Errorf("%s", str))
+		}
 
 
-	for _, res := range resGroup.Resources {
 		if validator, ok := driverValidators[res.Driver]; ok {
 		if validator, ok := driverValidators[res.Driver]; ok {
 			if err := validator(res); err != nil {
 			if err := validator(res); err != nil {
 				errors = append(errors, err)
 				errors = append(errors, err)
@@ -75,31 +75,3 @@ func Validate(contents string) []error {
 
 
 	return errors
 	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)
-}