diff --git a/installer/cmd/render.go b/installer/cmd/render.go index 4f27e5aab4b2b0..d44fc391ae45d6 100644 --- a/installer/cmd/render.go +++ b/installer/cmd/render.go @@ -20,8 +20,9 @@ import ( ) var renderOpts struct { - ConfigFN string - Namespace string + ConfigFN string + Namespace string + ValidateConfigDisabled bool } //go:embed versions.yaml @@ -40,16 +41,10 @@ A config file is required which can be generated with the init command.`, # Install Gitpod into a non-default namespace. gitpod-installer render --config config.yaml --namespace gitpod | kubectl apply -f -`, RunE: func(cmd *cobra.Command, args []string) error { - cfgFN, _ := cmd.PersistentFlags().GetString("config") - - rawCfg, cfgVersion, err := config.Load(cfgFN) + _, cfgVersion, cfg, err := loadConfig(renderOpts.ConfigFN) if err != nil { - return fmt.Errorf("error loading config: %w", err) - } - if cfgVersion != config.CurrentVersion { - return fmt.Errorf("config version is mismatch: expected %s, got %s", config.CurrentVersion, cfgVersion) + return err } - cfg := rawCfg.(*configv1.Config) var versionMF versions.Manifest err = yaml.Unmarshal(versionManifest, &versionMF) @@ -57,12 +52,16 @@ A config file is required which can be generated with the init command.`, return err } - namespace, _ := cmd.PersistentFlags().GetString("namespace") + if !renderOpts.ValidateConfigDisabled { + if err = runConfigValidation(cfgVersion, cfg); err != nil { + return err + } + } ctx := &common.RenderContext{ Config: *cfg, VersionManifest: versionMF, - Namespace: namespace, + Namespace: renderOpts.Namespace, } var renderable common.RenderFunc @@ -108,9 +107,25 @@ A config file is required which can be generated with the init command.`, }, } +func loadConfig(cfgFN string) (rawCfg interface{}, cfgVersion string, cfg *configv1.Config, err error) { + rawCfg, cfgVersion, err = config.Load(cfgFN) + if err != nil { + err = fmt.Errorf("error loading config: %w", err) + return + } + if cfgVersion != config.CurrentVersion { + err = fmt.Errorf("config version is mismatch: expected %s, got %s", config.CurrentVersion, cfgVersion) + return + } + cfg = rawCfg.(*configv1.Config) + + return rawCfg, cfgVersion, cfg, err +} + func init() { rootCmd.AddCommand(renderCmd) renderCmd.PersistentFlags().StringVarP(&renderOpts.ConfigFN, "config", "c", os.Getenv("GITPOD_INSTALLER_CONFIG"), "path to the config file") renderCmd.PersistentFlags().StringVarP(&renderOpts.Namespace, "namespace", "n", "default", "namespace to deploy to") + renderCmd.Flags().BoolVar(&renderOpts.ValidateConfigDisabled, "no-validation", false, "if set, the config will not be validated before running") } diff --git a/installer/cmd/validate_config.go b/installer/cmd/validate_config.go new file mode 100644 index 00000000000000..f40cd8cbfe03c7 --- /dev/null +++ b/installer/cmd/validate_config.go @@ -0,0 +1,77 @@ +// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package cmd + +import ( + "fmt" + "github.com/gitpod-io/gitpod/installer/pkg/config" + "github.com/spf13/cobra" + "os" + "strings" +) + +var validateConfigOpts struct { + Config string +} + +// validateConfigCmd represents the cluster command +var validateConfigCmd = &cobra.Command{ + Use: "config", + Short: "Validate the deployment configuration", + RunE: func(cmd *cobra.Command, args []string) error { + _, cfgVersion, cfg, err := loadConfig(validateConfigOpts.Config) + if err != nil { + return err + } + + if err = runConfigValidation(cfgVersion, cfg); err != nil { + return err + } + + fmt.Println("Gitpod installer configuration valid") + + return nil + }, +} + +// runConfigValidation this will run the validation and print any validation errors +// It's silent if everything is fine +func runConfigValidation(version string, cfg interface{}) error { + apiVersion, err := config.LoadConfigVersion(version) + if err != nil { + return err + } + + validationErrs, err := config.Validate(apiVersion, cfg) + if err != nil { + return err + } + + if len(validationErrs) > 0 { + for _, v := range validationErrs { + switch v.Tag() { + case "required": + fmt.Println(fmt.Sprintf("Field '%s' is required", v.Namespace())) + case "required_if", "required_unless", "required_with": + tag := strings.Replace(v.Tag(), "_", " ", -1) + fmt.Println(fmt.Sprintf("Field '%s' is %s '%s'", v.Namespace(), tag, v.Param())) + case "startswith": + fmt.Println(fmt.Sprintf("Field '%s' must start with '%s'", v.Namespace(), v.Param())) + default: + // General error message + fmt.Println(fmt.Sprintf("Field '%s' failed %s validation", v.Namespace(), v.Tag())) + } + } + return fmt.Errorf("configuration invalid") + } + + return nil +} + +func init() { + validateCmd.AddCommand(validateConfigCmd) + + validateCmd.PersistentFlags().StringVarP(&validateConfigOpts.Config, "config", "c", os.Getenv("GITPOD_INSTALLER_CONFIG"), "path to the config file") +} diff --git a/installer/example-config.yaml b/installer/example-config.yaml index 2aafb4fe6faeeb..8bdc474851aafa 100644 --- a/installer/example-config.yaml +++ b/installer/example-config.yaml @@ -2,8 +2,8 @@ apiVersion: v1 blockNewUsers: enabled: false certificate: - kind: "" - name: "https-certificates" + kind: secret + name: https-certificates containerRegistry: inCluster: true database: @@ -11,10 +11,8 @@ database: domain: gitpod.example.com imagePullSecrets: null jaegerOperator: - InCluster: true + inCluster: true kind: Full -messageQueue: - InCluster: true metadata: region: "" objectStorage: diff --git a/installer/go.mod b/installer/go.mod index 422e71ee217cd2..de6afc25bb2310 100644 --- a/installer/go.mod +++ b/installer/go.mod @@ -16,6 +16,7 @@ require ( github.com/gitpod-io/gitpod/ws-manager/api v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/ws-proxy v0.0.0-00010101000000-000000000000 github.com/gitpod-io/gitpod/ws-scheduler v0.0.0-00010101000000-000000000000 + github.com/go-playground/validator/v10 v10.9.0 github.com/google/go-cmp v0.5.6 github.com/jetstack/cert-manager v1.4.4 github.com/spf13/cobra v1.2.1 @@ -93,6 +94,8 @@ require ( github.com/go-openapi/jsonreference v0.19.5 // indirect github.com/go-openapi/swag v0.19.14 // indirect github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/gogo/googleapis v1.4.0 // indirect @@ -123,6 +126,7 @@ require ( github.com/klauspost/cpuid v1.3.1 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/lib/pq v1.10.3 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.6 // indirect diff --git a/installer/go.sum b/installer/go.sum index 5c289858e231ef..17c1f146921ad9 100644 --- a/installer/go.sum +++ b/installer/go.sum @@ -472,6 +472,14 @@ github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/ github.com/go-ozzo/ozzo-validation v3.5.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.9.0 h1:NgTtmN58D0m8+UuxtYmGztBJB7VnPgjj221I1QHci2A= +github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -749,8 +757,9 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -760,6 +769,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= @@ -965,6 +976,7 @@ github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoU github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -1025,6 +1037,9 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc h1:BD7uZqkN8CpjJtN/tScAKiccBikU4dlqe/gNrkRaPY4= @@ -1467,6 +1482,7 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= diff --git a/installer/pkg/config/loader.go b/installer/pkg/config/loader.go index 10aebfa55ef04b..429ca0eeb13a0b 100644 --- a/installer/pkg/config/loader.go +++ b/installer/pkg/config/loader.go @@ -6,6 +6,7 @@ package config import ( "fmt" + "github.com/go-playground/validator/v10" "io/ioutil" "sigs.k8s.io/yaml" @@ -38,6 +39,9 @@ type ConfigVersion interface { // Defaults fills in the defaults for this version. // obj is expected to be the return value of Factory() Defaults(obj interface{}) error + + // LoadValidationFuncs loads the custom validation functions + LoadValidationFuncs(*validator.Validate) error } // AddVersion adds a new version. @@ -55,6 +59,15 @@ var ( var versions map[string]ConfigVersion +func LoadConfigVersion(version string) (ConfigVersion, error) { + v, ok := versions[version] + if !ok { + return nil, fmt.Errorf("unsupprted API version: %s", version) + } + + return v, nil +} + func Load(fn string) (cfg interface{}, version string, err error) { fc, err := ioutil.ReadFile(fn) if err != nil { @@ -68,11 +81,11 @@ func Load(fn string) (cfg interface{}, version string, err error) { return } - v, ok := versions[vs.APIVersion] - if !ok { - err = fmt.Errorf("unsupprted API version: %s", vs.APIVersion) + v, err := LoadConfigVersion(vs.APIVersion) + if err != nil { return } + cfg = v.Factory() version = vs.APIVersion err = yaml.Unmarshal(fc, cfg) diff --git a/installer/pkg/config/v1/config.go b/installer/pkg/config/v1/config.go index 43e20fb1811c8a..b78f383983b0e2 100644 --- a/installer/pkg/config/v1/config.go +++ b/installer/pkg/config/v1/config.go @@ -7,7 +7,6 @@ package config import ( "github.com/gitpod-io/gitpod/installer/pkg/config" "github.com/gitpod-io/gitpod/ws-daemon/pkg/resources" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/utils/pointer" @@ -30,8 +29,10 @@ func (v version) Defaults(in interface{}) error { cfg.Domain = "gitpod.example.com" cfg.Repository = "eu.gcr.io/gitpod-core-dev/build" cfg.Observability = Observability{ - LogLevel: LogLevel("info"), + LogLevel: LogLevelInfo, } + cfg.Certificate.Kind = CertificateRefSecret + cfg.Certificate.Name = "https-certificates" cfg.Database.InCluster = pointer.Bool(true) cfg.ObjectStorage.InCluster = pointer.Bool(true) cfg.ContainerRegistry.InCluster = pointer.Bool(true) @@ -47,27 +48,27 @@ func (v version) Defaults(in interface{}) error { } type Config struct { - Kind InstallationKind `json:"kind"` - Domain string `json:"domain"` + Kind InstallationKind `json:"kind" validate:"required,installationKind"` + Domain string `json:"domain" validate:"required,fqdn"` Metadata Metadata `json:"metadata"` - Repository string `json:"repository"` + Repository string `json:"repository" validate:"required,ascii"` Observability Observability `json:"observability"` Analytics *Analytics `json:"analytics,omitempty"` - Database Database `json:"database"` + Database Database `json:"database" validate:"required"` - ObjectStorage ObjectStorage `json:"objectStorage"` + ObjectStorage ObjectStorage `json:"objectStorage" validate:"required"` - ContainerRegistry ContainerRegistry `json:"containerRegistry"` + ContainerRegistry ContainerRegistry `json:"containerRegistry" validate:"required"` - Jaeger Jaeger `json:"jaegerOperator"` + Jaeger Jaeger `json:"jaegerOperator" validate:"required"` - Certificate ObjectRef `json:"certificate"` + Certificate ObjectRef `json:"certificate" validate:"required"` ImagePullSecrets []ObjectRef `json:"imagePullSecrets"` - Workspace Workspace `json:"workspace"` + Workspace Workspace `json:"workspace" validate:"required"` AuthProviders []AuthProviderConfigs `json:"authProviders,omitempty"` BlockNewUsers BlockNewUsers `json:"blockNewUsers"` @@ -78,7 +79,7 @@ type Metadata struct { } type Observability struct { - LogLevel LogLevel `json:"logLevel"` + LogLevel LogLevel `json:"logLevel" validate:"required,logLevel"` Tracing *Tracing `json:"tracing,omitempty"` } @@ -129,8 +130,8 @@ const ( ) type ObjectRef struct { - Kind CertificateRefKind `json:"kind"` - Name string `json:"name"` + Kind CertificateRefKind `json:"kind" validate:"required,certificateKind"` + Name string `json:"name" validate:"required"` } type CertificateRefKind string @@ -140,37 +141,49 @@ const ( ) type ContainerRegistry struct { - InCluster *bool `json:"inCluster,omitempty"` - External *ContainerRegistryExternal `json:"external,omitempty"` + InCluster *bool `json:"inCluster,omitempty" validate:"required"` + External *ContainerRegistryExternal `json:"external,omitempty" validate:"required_if=InCluster false"` } type ContainerRegistryExternal struct { - URL string `json:"url"` - Certificate ObjectRef `json:"certificate"` + URL string `json:"url" validate:"required"` + Certificate ObjectRef `json:"certificate" validate:"required"` } type Jaeger struct { - InCluster *bool `json:"inCluster,omitempty"` - External *JaegerOperatorExternal `json:"external,omitempty"` + InCluster *bool `json:"inCluster,omitempty" validate:"required"` + External *JaegerOperatorExternal `json:"external,omitempty" validate:"required_if=InCluster false"` } type JaegerOperatorExternal struct { - Certificate ObjectRef `json:"certificate"` + Certificate ObjectRef `json:"certificate" validate:"required"` } type LogLevel string +// Taken from github.com/gitpod-io/gitpod/components/gitpod-protocol/src/util/logging.ts +const ( + LogLevelTrace LogLevel = "trace" + LogLevelDebug LogLevel = "debug" + LogLevelInfo LogLevel = "info" + LogLevelWarning LogLevel = "warning" + LogLevelError LogLevel = "error" + LogLevelFatal LogLevel = "fatal" + LogLevelPanic LogLevel = "panic" +) + type Resources struct { - Requests corev1.ResourceList `json:"requests"` + // todo(sje): add custom validation to corev1.ResourceList + Requests corev1.ResourceList `json:"requests" validate:"required"` Limits corev1.ResourceList `json:"limits,omitempty"` DynamicLimits *struct { - CPU []resources.Bucket + CPU []resources.Bucket // todo(sje): add custom validation } `json:"dynamicLimits,omitempty"` } type WorkspaceRuntime struct { - FSShiftMethod FSShiftMethod `json:"fsShiftMethod"` - ContainerDRuntimeDir string `json:"containerdRuntimeDir"` + FSShiftMethod FSShiftMethod `json:"fsShiftMethod" validate:"required,fsShiftMethod"` + ContainerDRuntimeDir string `json:"containerdRuntimeDir" validate:"required,startswith=/"` } type WorkspaceTemplates struct { @@ -182,8 +195,8 @@ type WorkspaceTemplates struct { } type Workspace struct { - Runtime WorkspaceRuntime `json:"runtime"` - Resources Resources `json:"resources"` + Runtime WorkspaceRuntime `json:"runtime" validate:"required"` + Resources Resources `json:"resources" validate:"required"` Templates *WorkspaceTemplates `json:"templates,omitempty"` } diff --git a/installer/pkg/config/v1/validation.go b/installer/pkg/config/v1/validation.go new file mode 100644 index 00000000000000..e7b834d4c9c8a6 --- /dev/null +++ b/installer/pkg/config/v1/validation.go @@ -0,0 +1,69 @@ +// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package config + +import ( + "github.com/go-playground/validator/v10" +) + +var InstallationKindList = map[InstallationKind]struct{}{ + InstallationMeta: {}, + InstallationWorkspace: {}, + InstallationFull: {}, +} + +var LogLevelList = map[LogLevel]struct{}{ + LogLevelTrace: {}, + LogLevelDebug: {}, + LogLevelInfo: {}, + LogLevelWarning: {}, + LogLevelError: {}, + LogLevelFatal: {}, + LogLevelPanic: {}, +} + +var CertificateRefKindList = map[CertificateRefKind]struct{}{ + CertificateRefSecret: {}, +} + +var FSShiftMethodList = map[FSShiftMethod]struct{}{ + FSShiftFuseFS: {}, + FSShiftShiftFS: {}, +} + +// LoadValidationFuncs load custom validation functions for this version of the config API +func (v version) LoadValidationFuncs(validate *validator.Validate) error { + var err error + + if err = validate.RegisterValidation("certificateKind", func(fl validator.FieldLevel) bool { + _, ok := CertificateRefKindList[CertificateRefKind(fl.Field().String())] + return ok + }); err != nil { + return err + } + + if err = validate.RegisterValidation("fsShiftMethod", func(fl validator.FieldLevel) bool { + _, ok := FSShiftMethodList[FSShiftMethod(fl.Field().String())] + return ok + }); err != nil { + return err + } + + if err = validate.RegisterValidation("installationKind", func(fl validator.FieldLevel) bool { + _, ok := InstallationKindList[InstallationKind(fl.Field().String())] + return ok + }); err != nil { + return err + } + + if err = validate.RegisterValidation("logLevel", func(fl validator.FieldLevel) bool { + _, ok := LogLevelList[LogLevel(fl.Field().String())] + return ok + }); err != nil { + return err + } + + return nil +} diff --git a/installer/pkg/config/validation.go b/installer/pkg/config/validation.go new file mode 100644 index 00000000000000..0e76ecf76436cb --- /dev/null +++ b/installer/pkg/config/validation.go @@ -0,0 +1,27 @@ +// Copyright (c) 2021 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package config + +import ( + "github.com/go-playground/validator/v10" +) + +func Validate(version ConfigVersion, cfg interface{}) ([]validator.FieldError, error) { + validate := validator.New() + err := version.LoadValidationFuncs(validate) + if err != nil { + return nil, err + } + + err = validate.Struct(cfg) + + if err != nil { + validationErrors := err.(validator.ValidationErrors) + + return validationErrors, nil + } + + return nil, nil +}