Merge branch '18947-githttpd'
[arvados.git] / lib / config / load.go
index 956a47b1a4ac2ef992958739d5189eaf5e519ed5..6099215edc2f4d4fb5c014d2ee6b48aa327d730e 100644 (file)
@@ -6,6 +6,7 @@ package config
 
 import (
        "bytes"
+       _ "embed"
        "encoding/json"
        "errors"
        "flag"
@@ -14,6 +15,7 @@ import (
        "io/ioutil"
        "os"
        "regexp"
+       "strconv"
        "strings"
 
        "git.arvados.org/arvados.git/sdk/go/arvados"
@@ -22,6 +24,9 @@ import (
        "github.com/sirupsen/logrus"
 )
 
+//go:embed config.default.yml
+var DefaultYAML []byte
+
 var ErrNoClustersDefined = errors.New("config does not define any clusters")
 
 type Loader struct {
@@ -71,7 +76,7 @@ func (ldr *Loader) SetupFlags(flagset *flag.FlagSet) {
                flagset.StringVar(&ldr.CrunchDispatchSlurmPath, "legacy-crunch-dispatch-slurm-config", defaultCrunchDispatchSlurmConfigPath, "Legacy crunch-dispatch-slurm configuration `file`")
                flagset.StringVar(&ldr.WebsocketPath, "legacy-ws-config", defaultWebsocketConfigPath, "Legacy arvados-ws configuration `file`")
                flagset.StringVar(&ldr.KeepproxyPath, "legacy-keepproxy-config", defaultKeepproxyConfigPath, "Legacy keepproxy configuration `file`")
-               flagset.StringVar(&ldr.GitHttpdPath, "legacy-git-httpd-config", defaultGitHttpdConfigPath, "Legacy arv-git-httpd configuration `file`")
+               flagset.StringVar(&ldr.GitHttpdPath, "legacy-git-httpd-config", defaultGitHttpdConfigPath, "Legacy arvados-git-httpd configuration `file`")
                flagset.StringVar(&ldr.KeepBalancePath, "legacy-keepbalance-config", defaultKeepBalanceConfigPath, "Legacy keep-balance configuration `file`")
                flagset.BoolVar(&ldr.SkipLegacy, "skip-legacy", false, "Don't load legacy config files")
        }
@@ -225,6 +230,9 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
        }
        ldr.logExtraKeys(merged, src, "")
        removeSampleKeys(merged)
+       // We merge the loaded config into the default, overriding any existing keys.
+       // Make sure we do not override a default with a key that has a 'null' value.
+       removeNullKeys(src)
        err = mergo.Merge(&merged, src, mergo.WithOverride)
        if err != nil {
                return nil, fmt.Errorf("merging config data: %s", err)
@@ -277,6 +285,19 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
                }
        }
 
+       // Preprocess/automate some configs
+       for id, cc := range cfg.Clusters {
+               ldr.autofillPreemptible("Clusters."+id, &cc)
+
+               if strings.Count(cc.Users.AnonymousUserToken, "/") == 3 {
+                       // V2 token, strip it to just a secret
+                       tmp := strings.Split(cc.Users.AnonymousUserToken, "/")
+                       cc.Users.AnonymousUserToken = tmp[2]
+               }
+
+               cfg.Clusters[id] = cc
+       }
+
        // Check for known mistakes
        for id, cc := range cfg.Clusters {
                for remote := range cc.RemoteClusters {
@@ -291,14 +312,16 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
                for _, err = range []error{
                        ldr.checkClusterID(fmt.Sprintf("Clusters.%s", id), id, false),
                        ldr.checkClusterID(fmt.Sprintf("Clusters.%s.Login.LoginCluster", id), cc.Login.LoginCluster, true),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.ManagementToken", id), cc.ManagementToken),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.SystemRootToken", id), cc.SystemRootToken),
-                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Collections.BlobSigningKey", id), cc.Collections.BlobSigningKey),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.ManagementToken", id), cc.ManagementToken, true, false),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.SystemRootToken", id), cc.SystemRootToken, true, false),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Users.AnonymousUserToken", id), cc.Users.AnonymousUserToken, false, true),
+                       ldr.checkToken(fmt.Sprintf("Clusters.%s.Collections.BlobSigningKey", id), cc.Collections.BlobSigningKey, true, false),
                        checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection),
                        ldr.checkEnum("Containers.LocalKeepLogsToContainerLog", cc.Containers.LocalKeepLogsToContainerLog, "none", "all", "errors"),
                        ldr.checkEmptyKeepstores(cc),
                        ldr.checkUnlistedKeepstores(cc),
                        ldr.checkStorageClasses(cc),
+                       ldr.checkCUDAVersions(cc),
                        // TODO: check non-empty Rendezvous on
                        // services other than Keepstore
                } {
@@ -324,13 +347,34 @@ func (ldr *Loader) checkClusterID(label, clusterID string, emptyStringOk bool) e
 var acceptableTokenRe = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
 var acceptableTokenLength = 32
 
-func (ldr *Loader) checkToken(label, token string) error {
-       if token == "" {
-               if ldr.Logger != nil {
-                       ldr.Logger.Warnf("%s: secret token is not set (use %d+ random characters from a-z, A-Z, 0-9)", label, acceptableTokenLength)
+func (ldr *Loader) checkToken(label, token string, mandatory bool, acceptV2 bool) error {
+       if len(token) == 0 {
+               if !mandatory {
+                       // when a token is not mandatory, the acceptable length and content is only checked if its length is non-zero
+                       return nil
+               } else {
+                       if ldr.Logger != nil {
+                               ldr.Logger.Warnf("%s: secret token is not set (use %d+ random characters from a-z, A-Z, 0-9)", label, acceptableTokenLength)
+                       }
                }
        } else if !acceptableTokenRe.MatchString(token) {
-               return fmt.Errorf("%s: unacceptable characters in token (only a-z, A-Z, 0-9 are acceptable)", label)
+               if !acceptV2 {
+                       return fmt.Errorf("%s: unacceptable characters in token (only a-z, A-Z, 0-9 are acceptable)", label)
+               }
+               // Test for a proper V2 token
+               tmp := strings.SplitN(token, "/", 3)
+               if len(tmp) != 3 {
+                       return fmt.Errorf("%s: unacceptable characters in token (only a-z, A-Z, 0-9 are acceptable)", label)
+               }
+               if !strings.HasPrefix(token, "v2/") {
+                       return fmt.Errorf("%s: unacceptable characters in token (only a-z, A-Z, 0-9 are acceptable)", label)
+               }
+               if !acceptableTokenRe.MatchString(tmp[2]) {
+                       return fmt.Errorf("%s: unacceptable characters in V2 token secret (only a-z, A-Z, 0-9 are acceptable)", label)
+               }
+               if len(tmp[2]) < acceptableTokenLength {
+                       ldr.Logger.Warnf("%s: secret is too short (should be at least %d characters)", label, acceptableTokenLength)
+               }
        } else if len(token) < acceptableTokenLength {
                if ldr.Logger != nil {
                        ldr.Logger.Warnf("%s: token is too short (should be at least %d characters)", label, acceptableTokenLength)
@@ -399,6 +443,24 @@ func (ldr *Loader) checkStorageClasses(cc arvados.Cluster) error {
        return nil
 }
 
+func (ldr *Loader) checkCUDAVersions(cc arvados.Cluster) error {
+       for _, it := range cc.InstanceTypes {
+               if it.CUDA.DeviceCount == 0 {
+                       continue
+               }
+
+               _, err := strconv.ParseFloat(it.CUDA.DriverVersion, 64)
+               if err != nil {
+                       return fmt.Errorf("InstanceType %q has invalid CUDA.DriverVersion %q, expected format X.Y (%v)", it.Name, it.CUDA.DriverVersion, err)
+               }
+               _, err = strconv.ParseFloat(it.CUDA.HardwareCapability, 64)
+               if err != nil {
+                       return fmt.Errorf("InstanceType %q has invalid CUDA.HardwareCapability %q, expected format X.Y (%v)", it.Name, it.CUDA.HardwareCapability, err)
+               }
+       }
+       return nil
+}
+
 func checkKeyConflict(label string, m map[string]string) error {
        saw := map[string]bool{}
        for k := range m {
@@ -411,6 +473,17 @@ func checkKeyConflict(label string, m map[string]string) error {
        return nil
 }
 
+func removeNullKeys(m map[string]interface{}) {
+       for k, v := range m {
+               if v == nil {
+                       delete(m, k)
+               }
+               if v, _ := v.(map[string]interface{}); v != nil {
+                       removeNullKeys(v)
+               }
+       }
+}
+
 func removeSampleKeys(m map[string]interface{}) {
        delete(m, "SAMPLE")
        for _, v := range m {
@@ -464,3 +537,21 @@ func (ldr *Loader) logExtraKeys(expected, supplied map[string]interface{}, prefi
                }
        }
 }
+
+func (ldr *Loader) autofillPreemptible(label string, cc *arvados.Cluster) {
+       if factor := cc.Containers.PreemptiblePriceFactor; factor > 0 {
+               for name, it := range cc.InstanceTypes {
+                       if !it.Preemptible {
+                               it.Preemptible = true
+                               it.Price = it.Price * factor
+                               it.Name = name + ".preemptible"
+                               if it2, exists := cc.InstanceTypes[it.Name]; exists && it2 != it {
+                                       ldr.Logger.Warnf("%s.InstanceTypes[%s]: already exists, so not automatically adding a preemptible variant of %s", label, it.Name, name)
+                                       continue
+                               }
+                               cc.InstanceTypes[it.Name] = it
+                       }
+               }
+       }
+
+}