summaryrefslogtreecommitdiff
path: root/registry-scanner/pkg/image/credentials.go
diff options
context:
space:
mode:
Diffstat (limited to 'registry-scanner/pkg/image/credentials.go')
-rw-r--r--registry-scanner/pkg/image/credentials.go261
1 files changed, 261 insertions, 0 deletions
diff --git a/registry-scanner/pkg/image/credentials.go b/registry-scanner/pkg/image/credentials.go
new file mode 100644
index 0000000..a19d01a
--- /dev/null
+++ b/registry-scanner/pkg/image/credentials.go
@@ -0,0 +1,261 @@
+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 <username>:<password>")
+ }
+ 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)
+}