package image import ( "encoding/base64" "encoding/json" "fmt" "os" "os/exec" "strings" "time" argoexec "github.com/argoproj/pkg/exec" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/kube" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" ) type CredentialSourceType int const ( CredentialSourceUnknown CredentialSourceType = 0 CredentialSourcePullSecret CredentialSourceType = 1 CredentialSourceSecret CredentialSourceType = 2 CredentialSourceEnv CredentialSourceType = 3 CredentialSourceExt CredentialSourceType = 4 ) type CredentialSource struct { Type CredentialSourceType Registry string SecretNamespace string SecretName string SecretField string EnvName string ScriptPath string } type Credential struct { Username string Password string } const pullSecretField = ".dockerconfigjson" // gcr.io=secret:foo/bar#baz // gcr.io=pullsecret:foo/bar // gcr.io=env:FOOBAR func ParseCredentialSource(credentialSource string, requirePrefix bool) (*CredentialSource, error) { src := CredentialSource{} var secretDef string tokens := strings.SplitN(credentialSource, "=", 2) if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { if requirePrefix { return nil, fmt.Errorf("invalid credential spec: %s", credentialSource) } secretDef = credentialSource } else { src.Registry = tokens[0] secretDef = tokens[1] } tokens = strings.Split(secretDef, ":") if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { return nil, fmt.Errorf("invalid credential spec: %s", credentialSource) } var err error switch strings.ToLower(tokens[0]) { case "secret": err = src.parseSecretDefinition(tokens[1]) src.Type = CredentialSourceSecret case "pullsecret": err = src.parsePullSecretDefinition(tokens[1]) src.Type = CredentialSourcePullSecret case "env": err = src.parseEnvDefinition(tokens[1]) src.Type = CredentialSourceEnv case "ext": err = src.parseExtDefinition(tokens[1]) src.Type = CredentialSourceExt default: err = fmt.Errorf("unknown credential source: %s", tokens[0]) } if err != nil { return nil, err } return &src, nil } // FetchCredentials fetches the credentials for a given registry according to // the credential source. func (src *CredentialSource) FetchCredentials(registryURL string, kubeclient *kube.KubernetesClient) (*Credential, error) { var creds Credential log.Tracef("Fetching credentials for registry %s", registryURL) switch src.Type { case CredentialSourceEnv: credEnv := os.Getenv(src.EnvName) if credEnv == "" { return nil, fmt.Errorf("could not fetch credentials: env '%s' is not set", src.EnvName) } tokens := strings.SplitN(credEnv, ":", 2) if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { return nil, fmt.Errorf("could not fetch credentials: value of %s is malformed", src.EnvName) } creds.Username = tokens[0] creds.Password = tokens[1] return &creds, nil case CredentialSourceSecret: if kubeclient == nil { return nil, fmt.Errorf("could not fetch credentials: no Kubernetes client given") } data, err := kubeclient.GetSecretField(src.SecretNamespace, src.SecretName, src.SecretField) if err != nil { return nil, fmt.Errorf("could not fetch secret '%s' from namespace '%s' (field: '%s'): %v", src.SecretName, src.SecretNamespace, src.SecretField, err) } tokens := strings.SplitN(data, ":", 2) if len(tokens) != 2 { return nil, fmt.Errorf("invalid credentials in secret '%s' from namespace '%s' (field '%s')", src.SecretName, src.SecretNamespace, src.SecretField) } creds.Username = tokens[0] creds.Password = tokens[1] return &creds, nil case CredentialSourcePullSecret: if kubeclient == nil { return nil, fmt.Errorf("could not fetch credentials: no Kubernetes client given") } src.SecretField = pullSecretField data, err := kubeclient.GetSecretField(src.SecretNamespace, src.SecretName, src.SecretField) if err != nil { return nil, fmt.Errorf("could not fetch secret '%s' from namespace '%s' (field: '%s'): %v", src.SecretName, src.SecretNamespace, src.SecretField, err) } creds.Username, creds.Password, err = parseDockerConfigJson(registryURL, data) if err != nil { return nil, err } return &creds, nil case CredentialSourceExt: if !strings.HasPrefix(src.ScriptPath, "/") { return nil, fmt.Errorf("path to script must be absolute, but is '%s'", src.ScriptPath) } _, err := os.Stat(src.ScriptPath) if err != nil { return nil, fmt.Errorf("could not stat %s: %v", src.ScriptPath, err) } cmd := exec.Command(src.ScriptPath) out, err := argoexec.RunCommandExt(cmd, argoexec.CmdOpts{Timeout: 10 * time.Second}) if err != nil { return nil, fmt.Errorf("error executing %s: %v", src.ScriptPath, err) } tokens := strings.SplitN(out, ":", 2) if len(tokens) != 2 { return nil, fmt.Errorf("invalid script output, must be single line with syntax :") } creds.Username = tokens[0] creds.Password = tokens[1] return &creds, nil default: return nil, fmt.Errorf("unknown credential type") } } // Parse a secret definition in form of 'namespace/name#field' func (src *CredentialSource) parseSecretDefinition(definition string) error { tokens := strings.Split(definition, "#") if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { return fmt.Errorf("invalid secret definition: %s", definition) } src.SecretField = tokens[1] tokens = strings.Split(tokens[0], "/") if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { return fmt.Errorf("invalid secret definition: %s", definition) } src.SecretNamespace = tokens[0] src.SecretName = tokens[1] return nil } // Parse an image pull secret definition in form of 'namespace/name' func (src *CredentialSource) parsePullSecretDefinition(definition string) error { tokens := strings.Split(definition, "/") if len(tokens) != 2 || tokens[0] == "" || tokens[1] == "" { return fmt.Errorf("invalid secret definition: %s", definition) } src.SecretNamespace = tokens[0] src.SecretName = tokens[1] src.SecretField = pullSecretField return nil } // Parse an environment definition // nolint:unparam func (src *CredentialSource) parseEnvDefinition(definition string) error { src.EnvName = definition return nil } // Parse an external script definition // nolint:unparam func (src *CredentialSource) parseExtDefinition(definition string) error { src.ScriptPath = definition return nil } // This unmarshals & parses Docker's config.json file, returning username and // password for given registry URL func parseDockerConfigJson(registryURL string, jsonSource string) (string, string, error) { var dockerConf map[string]interface{} err := json.Unmarshal([]byte(jsonSource), &dockerConf) if err != nil { return "", "", err } auths, ok := dockerConf["auths"].(map[string]interface{}) if !ok { return "", "", fmt.Errorf("no credentials in image pull secret") } var regPrefix string if strings.HasPrefix(registryURL, "http://") { regPrefix = strings.TrimPrefix(registryURL, "http://") } else if strings.HasPrefix(registryURL, "https://") { regPrefix = strings.TrimPrefix(registryURL, "https://") } else { regPrefix = registryURL } regPrefix = strings.TrimSuffix(regPrefix, "/") for registry, authConf := range auths { if !strings.HasPrefix(registry, registryURL) && !strings.HasPrefix(registry, regPrefix) { log.Tracef("found registry %s in image pull secret, but we want %s (%s) - skipping", registry, registryURL, regPrefix) continue } authEntry, ok := authConf.(map[string]interface{}) if !ok { return "", "", fmt.Errorf("invalid auth entry for registry entry %s ('auths' entry should be map)", registry) } authString, ok := authEntry["auth"].(string) if !ok { return "", "", fmt.Errorf("invalid auth token for registry entry %s ('auth' should be string')", registry) } authToken, err := base64.StdEncoding.DecodeString(authString) if err != nil { return "", "", fmt.Errorf("could not base64-decode auth data for registry entry %s: %v", registry, err) } tokens := strings.SplitN(string(authToken), ":", 2) if len(tokens) != 2 { return "", "", fmt.Errorf("invalid data after base64 decoding auth entry for registry entry %s", registry) } return tokens[0], tokens[1], nil } return "", "", fmt.Errorf("no valid auth entry for registry %s found in image pull secret", registryURL) }