Path: blob/main/install/installer/pkg/config/v1/validation.go
2501 views
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package config56import (7"context"8"fmt"9"regexp"1011"github.com/gitpod-io/gitpod/installer/pkg/cluster"12"github.com/gitpod-io/gitpod/installer/pkg/config/v1/experimental"13"golang.org/x/crypto/ssh"14"sigs.k8s.io/yaml"1516"github.com/go-playground/validator/v10"17corev1 "k8s.io/api/core/v1"18"k8s.io/client-go/rest"19)2021var InstallationKindList = map[InstallationKind]struct{}{22InstallationIDE: {},23InstallationWebApp: {},24InstallationMeta: {},25InstallationWorkspace: {},26InstallationFull: {},27}2829var LogLevelList = map[LogLevel]struct{}{30LogLevelTrace: {},31LogLevelDebug: {},32LogLevelInfo: {},33LogLevelWarning: {},34LogLevelError: {},35LogLevelFatal: {},36LogLevelPanic: {},37}3839var ObjectRefKindList = map[ObjectRefKind]struct{}{40ObjectRefSecret: {},41}4243var FSShiftMethodList = map[FSShiftMethod]struct{}{44FSShiftShiftFS: {},45}4647// LoadValidationFuncs load custom validation functions for this version of the config API48func (v version) LoadValidationFuncs(validate *validator.Validate) error {49funcs := map[string]validator.Func{50"objectref_kind": func(fl validator.FieldLevel) bool {51_, ok := ObjectRefKindList[ObjectRefKind(fl.Field().String())]52return ok53},54"fs_shift_method": func(fl validator.FieldLevel) bool {55_, ok := FSShiftMethodList[FSShiftMethod(fl.Field().String())]56return ok57},58"installation_kind": func(fl validator.FieldLevel) bool {59_, ok := InstallationKindList[InstallationKind(fl.Field().String())]60return ok61},62"log_level": func(fl validator.FieldLevel) bool {63_, ok := LogLevelList[LogLevel(fl.Field().String())]64return ok65},66"block_new_users_passlist": func(fl validator.FieldLevel) bool {67if !fl.Parent().FieldByName("Enabled").Bool() {68// Not enabled - it's valid69return true70}7172if fl.Field().Len() == 0 {73// No exceptions74return false75}7677// Use same regex as "fqdn"78// @link https://github.com/go-playground/validator/blob/c7e0172e0fd176bdc521afb5186818a7db6b77ac/regexes.go#L5279fqdnRegexStringRFC1123 := `^([a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})(\.[a-zA-Z0-9]{1}[a-zA-Z0-9-]{0,62})*?(\.[a-zA-Z]{1}[a-zA-Z0-9]{0,62})\.?$`80fqdnRegexRFC1123 := regexp.MustCompile(fqdnRegexStringRFC1123)8182for i := 0; i < fl.Field().Len(); i++ {83val := fl.Field().Index(i).String()8485if val == "" {86// Empty value87return false88}8990// Check that it validates as a fully-qualified domain name91valid := fqdnRegexRFC1123.MatchString(val)92if !valid {93return false94}95}96return true97},98}99100for k, v := range experimental.ValidationChecks {101funcs[k] = v102}103104for n, f := range funcs {105err := validate.RegisterValidation(n, f)106if err != nil {107return err108}109}110111return nil112}113114// ClusterValidation introduces configuration specific cluster validation checks115func (v version) ClusterValidation(rcfg interface{}) cluster.ValidationChecks {116cfg := rcfg.(*Config)117118var res cluster.ValidationChecks119res = append(res, cluster.CheckSecret(cfg.Certificate.Name, cluster.CheckSecretRequiredData("tls.crt", "tls.key")))120121res = append(res, cluster.ValidationCheck{122Name: "affinity labels",123Check: checkAffinityLabels(getAffinityListByKind(cfg.Kind)),124Description: "all required affinity node labels " + fmt.Sprint(getAffinityListByKind(cfg.Kind)) + " are present in the cluster",125})126127if cfg.ObjectStorage.CloudStorage != nil {128secretName := cfg.ObjectStorage.CloudStorage.ServiceAccount.Name129res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("service-account.json")))130}131132if cfg.ObjectStorage.S3 != nil {133secretName := cfg.ObjectStorage.S3.Credentials.Name134res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("accessKeyId", "secretAccessKey")))135}136137if cfg.ContainerRegistry.External != nil {138secretName := cfg.ContainerRegistry.External.Certificate.Name139res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData(".dockerconfigjson")))140141if cfg.ContainerRegistry.External.Credentials != nil {142credSecretName := cfg.ContainerRegistry.External.Credentials.Name143res = append(res, cluster.CheckSecret(credSecretName, cluster.CheckSecretRequiredData("credentials")))144}145}146147if cfg.ContainerRegistry.S3Storage != nil {148secretName := cfg.ContainerRegistry.S3Storage.Certificate.Name149res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("s3AccessKey", "s3SecretKey")))150}151152if cfg.Database.CloudSQL != nil {153secretName := cfg.Database.CloudSQL.ServiceAccount.Name154res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("credentials.json", "encryptionKeys", "password", "username")))155}156157if cfg.Database.External != nil {158secretName := cfg.Database.External.Certificate.Name159res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("encryptionKeys", "host", "password", "port", "username")))160}161162if cfg.Database.SSL != nil && cfg.Database.SSL.CaCert != nil {163secretName := cfg.Database.SSL.CaCert.Name164res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData("ca.crt")))165}166167if len(cfg.AuthProviders) > 0 {168for _, provider := range cfg.AuthProviders {169secretName := provider.Name170secretKey := "provider"171res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRequiredData(secretKey), cluster.CheckSecretRule(func(s *corev1.Secret) ([]cluster.ValidationError, error) {172errors := make([]cluster.ValidationError, 0)173providerData := s.Data[secretKey]174175var provider AuthProviderConfigs176err := yaml.Unmarshal(providerData, &provider)177if err != nil {178return nil, err179}180181validate := validator.New()182err = v.LoadValidationFuncs(validate)183if err != nil {184return nil, err185}186187err = validate.Struct(provider)188if err != nil {189validationErrors := err.(validator.ValidationErrors)190191if len(validationErrors) > 0 {192for _, v := range validationErrors {193errors = append(errors, cluster.ValidationError{194Message: fmt.Sprintf("Field '%s' failed %s validation", v.Namespace(), v.Tag()),195Type: cluster.ValidationStatusError,196})197}198}199}200201return errors, nil202})))203}204}205206if cfg.SSHGatewayHostKey != nil {207secretName := cfg.SSHGatewayHostKey.Name208res = append(res, cluster.CheckSecret(secretName, cluster.CheckSecretRule(func(s *corev1.Secret) ([]cluster.ValidationError, error) {209var signers []ssh.Signer210errors := make([]cluster.ValidationError, 0)211for field, value := range s.Data {212hostSigner, err := ssh.ParsePrivateKey(value)213if err != nil {214errors = append(errors, cluster.ValidationError{215Message: fmt.Sprintf("Field '%s' can't parse to host key %v", field, err),216Type: cluster.ValidationStatusWarning,217})218continue219}220signers = append(signers, hostSigner)221}222if len(signers) == 0 {223errors = append(errors, cluster.ValidationError{224Message: fmt.Sprintf("Secret '%s' does not contain a valid host key", secretName),225Type: cluster.ValidationStatusError,226})227}228return errors, nil229})))230}231232res = append(res, experimental.ClusterValidation(cfg.Experimental)...)233234return res235}236237// checkAffinityLabels validates that the nodes have all the required affinity labels applied238// It assumes all the values are `true`239func checkAffinityLabels(targetAffinityList []string) func(context.Context, *rest.Config, string) ([]cluster.ValidationError, error) {240return func(ctx context.Context, config *rest.Config, namespace string) ([]cluster.ValidationError, error) {241nodes, err := cluster.ListNodesFromContext(ctx, config)242if err != nil {243return nil, err244}245246affinityList := map[string]bool{}247for _, affinity := range targetAffinityList {248affinityList[affinity] = false249}250251var res []cluster.ValidationError252for _, node := range nodes {253for k, v := range node.GetLabels() {254if _, found := affinityList[k]; found {255affinityList[k] = v == "true"256}257}258}259260// Check all the values in the map are `true`261for k, v := range affinityList {262if !v {263res = append(res, cluster.ValidationError{264Message: "Affinity label not found in cluster: " + k,265Type: cluster.ValidationStatusError,266})267}268}269return res, nil270}271}272273func getAffinityListByKind(kind InstallationKind) []string {274var affinityList []string275switch kind {276case InstallationMeta:277affinityList = cluster.AffinityListMeta278case InstallationWorkspace:279affinityList = cluster.AffinityListWorkspace280default:281affinityList = cluster.AffinityList282}283return affinityList284}285286287