diff --git a/Documentation/configuration.md b/Documentation/configuration.md index e066bf5..a52b0bd 100644 --- a/Documentation/configuration.md +++ b/Documentation/configuration.md @@ -44,7 +44,7 @@ be placed inside `auth.d` subdirectory (that is - in ##### Description and examples -This version if `auth` configuration specifies three additional +This version of `auth` configuration specifies three additional fields: `domains`, `type` and `credentials`. The `domains` field is an array of strings describing hosts for which @@ -149,7 +149,109 @@ In `/etc/rkt/auth.d/specific-tectonic.json`: } ``` -The result is that when downloading data from `kubernetes.io` we still -send `Authorization: Bearer common-token`, but when downloading from -`coreos.com` - `Authorization: Basic Zm9vOmJhcg==`. And for -`tectonic.com` - `Authorization: Bearer tectonic-token`. +The result is that when downloading data from `kubernetes.io` `rkt` +still sends `Authorization: Bearer common-token`, but when downloading +from `coreos.com` - `Authorization: Basic Zm9vOmJhcg==` (`foo:bar` +encoded in base64). And for `tectonic.com` - `Authorization: Bearer +tectonic-token`. + +### rktKind: `dockerAuth` + +This kind of configuration is used to set up necessary credentials +when downloading data from docker registries. The configuration files +should be placed inside `auth.d` subdirectory (that is - in +`/usr/lib/rkt/auth.d` or in `/etc/rkt/auth.d`). + +#### rktVersion: `v1` + +##### Description and examples + +This version of `dockerAuth` configuration specifies two additional +fields: `registries` and `credentials`. + +The `registries` field is an array of strings describing docker +registries for which following credentials should be used. A short +list of popular docker registries is below. This field has to be +specified and cannot be empty. + +`credentials` field holds the necessary data to authenticate against +docker registry. This field has to be specified and cannot be empty. + +Currently docker registries only support basic HTTP authentication, so +`credentials` field has two subfields - `user` and `password`. These +fields have to be specified and cannot be empty. + +Some popular docker registries: +* index.docker.io (this is is used when no docker registry is + specified in URL, like in `docker://redis`) +* quay.io +* gcr.io + +Example of dockerAuth config: +``` +{ + "rktKind": "dockerAuth", + "rktVersion": "v1", + "registries": ["index.docker.io", "quay.io"], + "credentials": { + "user": "foo", + "password": "bar" + } +} +``` + +##### Overriding semantics + +Overriding is done for each registry. That means that the user can +override credentials used for each registry. Example of vendor +configuration: + +In `/usr/lib/rkt/auth.d/docker.json`: +``` +{ + "rktKind": "dockerAuth", + "rktVersion": "v1", + "registries": ["index.docker.io", "gcr.io", "quay.io"], + "credentials": { + "user": "foo", + "password": "bar" + } +} +``` + +If only this configuration file were available to `rkt` then when +downloading images from either `index.docker.io`, `gcr.io` or +`quay.io`, `rkt` would use user `foo` and password `bar`. + +But with additional configuration like follows situation +changes. Example of custom configuration: + +In `/etc/rkt/auth.d/specific-quay.json`: +``` +{ + "rktKind": "dockerAuth", + "rktVersion": "v1", + "registries": ["quay.io"], + "credentials": { + "user": "baz", + "password": "quux" + } +} +``` +In `/etc/rkt/auth.d/specific-gcr.json`: +``` +{ + "rktKind": "dockerAuth", + "rktVersion": "v1", + "domains": ["gcr.io"], + "credentials": { + "user": "goo", + "password": "gle" + } +} +``` + +The result is that when downloading images from `index.docker.io` `rkt` +still sends user `foo` and password `bar`, but when downloading +from `quay.io` - user `baz` and password `quux`. And for +`gcr.io` - user `goo` and password `gle`. diff --git a/rkt/config/auth.go b/rkt/config/auth.go index 2910db9..e7e09ad 100644 --- a/rkt/config/auth.go +++ b/rkt/config/auth.go @@ -42,9 +42,17 @@ type oauthV1 struct { Token string `json:"token"` } +type dockerAuthV1JsonParser struct{} + +type dockerAuthV1 struct { + Registries []string `json:"registries"` + Credentials basicV1 `json:"credentials"` +} + func init() { addParser("auth", "v1", &authV1JsonParser{}) - registerSubDir("auth.d", []string{"auth"}) + addParser("dockerAuth", "v1", &dockerAuthV1JsonParser{}) + registerSubDir("auth.d", []string{"auth", "dockerAuth"}) } type basicAuthHeaderer struct { @@ -89,9 +97,9 @@ func (p *authV1JsonParser) parse(config *Config, raw []byte) error { ) switch auth.Type { case "basic": - headerer, err = p.getBasicV1Headerer(config, auth.Credentials) + headerer, err = p.getBasicV1Headerer(auth.Credentials) case "oauth": - headerer, err = p.getOAuthV1Headerer(config, auth.Credentials) + headerer, err = p.getOAuthV1Headerer(auth.Credentials) default: err = fmt.Errorf("unknown auth type: %q", auth.Type) } @@ -107,16 +115,13 @@ func (p *authV1JsonParser) parse(config *Config, raw []byte) error { return nil } -func (p *authV1JsonParser) getBasicV1Headerer(config *Config, raw json.RawMessage) (Headerer, error) { +func (p *authV1JsonParser) getBasicV1Headerer(raw json.RawMessage) (Headerer, error) { var basic basicV1 if err := json.Unmarshal(raw, &basic); err != nil { return nil, err } - if len(basic.User) == 0 { - return nil, fmt.Errorf("user not specified") - } - if len(basic.Password) == 0 { - return nil, fmt.Errorf("password not specified") + if err := validateBasicV1(&basic); err != nil { + return nil, err } return &basicAuthHeaderer{ user: basic.User, @@ -124,7 +129,7 @@ func (p *authV1JsonParser) getBasicV1Headerer(config *Config, raw json.RawMessag }, nil } -func (p *authV1JsonParser) getOAuthV1Headerer(config *Config, raw json.RawMessage) (Headerer, error) { +func (p *authV1JsonParser) getOAuthV1Headerer(raw json.RawMessage) (Headerer, error) { var oauth oauthV1 if err := json.Unmarshal(raw, &oauth); err != nil { return nil, err @@ -136,3 +141,40 @@ func (p *authV1JsonParser) getOAuthV1Headerer(config *Config, raw json.RawMessag token: oauth.Token, }, nil } + +func (p *dockerAuthV1JsonParser) parse(config *Config, raw []byte) error { + var auth dockerAuthV1 + if err := json.Unmarshal(raw, &auth); err != nil { + return err + } + if len(auth.Registries) == 0 { + return fmt.Errorf("no registries specified") + } + if err := validateBasicV1(&auth.Credentials); err != nil { + return err + } + basic := BasicCredentials{ + User: auth.Credentials.User, + Password: auth.Credentials.Password, + } + for _, registry := range auth.Registries { + if _, ok := config.DockerCredentialsPerRegistry[registry]; ok { + return fmt.Errorf("credentials for docker registry %q are already specified", registry) + } + config.DockerCredentialsPerRegistry[registry] = basic + } + return nil +} + +func validateBasicV1(basic *basicV1) error { + if basic == nil { + return fmt.Errorf("no credentials") + } + if len(basic.User) == 0 { + return fmt.Errorf("user not specified") + } + if len(basic.Password) == 0 { + return fmt.Errorf("password not specified") + } + return nil +} diff --git a/rkt/config/config.go b/rkt/config/config.go index f66a321..95a5860 100644 --- a/rkt/config/config.go +++ b/rkt/config/config.go @@ -31,10 +31,16 @@ type Headerer interface { Header() http.Header } +type BasicCredentials struct { + User string + Password string +} + // Config is a single place where configuration for rkt frontend needs // resides. type Config struct { - AuthPerHost map[string]Headerer + AuthPerHost map[string]Headerer + DockerCredentialsPerRegistry map[string]BasicCredentials } type configParser interface { @@ -140,7 +146,8 @@ func GetConfigFromDir(dir string) (*Config, error) { func newConfig() *Config { return &Config{ - AuthPerHost: make(map[string]Headerer), + AuthPerHost: make(map[string]Headerer), + DockerCredentialsPerRegistry: make(map[string]BasicCredentials), } } @@ -272,4 +279,7 @@ func mergeConfigs(config *Config, subconfig *Config) { for host, headerer := range subconfig.AuthPerHost { config.AuthPerHost[host] = headerer } + for registry, creds := range subconfig.DockerCredentialsPerRegistry { + config.DockerCredentialsPerRegistry[registry] = creds + } } diff --git a/rkt/config/config_test.go b/rkt/config/config_test.go index 9f43a0e..e55af02 100644 --- a/rkt/config/config_test.go +++ b/rkt/config/config_test.go @@ -73,26 +73,10 @@ func TestAuthConfigFormat(t *testing.T) { {`{"rktKind": "auth", "rktVersion": "v1", "domains": ["coreos.com"], "type": "oauth", "credentials": {"token": "sometoken"}}`, map[string]http.Header{"coreos.com": {"Authorization": []string{"Bearer sometoken"}}}, false}, } for _, tt := range tests { - f, err := tmpConfigFile(tstprefix) - if err != nil { - panic(fmt.Sprintf("Failed to create tmp config file: %v", err)) - } - defer f.Close() - if _, err := f.Write([]byte(tt.contents)); err != nil { - panic(fmt.Sprintf("Writing config to file failed: %v", err)) - } - fi, err := f.Stat() - if err != nil { - panic(fmt.Sprintf("Stating a tmp config file failed: %v", err)) - } - cfg := newConfig() - if err := readFile(cfg, fi, f.Name(), []string{"auth"}); err != nil { - if !tt.fail { - t.Errorf("Expected test to succeed, failed unexpectedly (contents: `%s`)", tt.contents) - } - } else if tt.fail { - t.Errorf("Expected test to fail, succeeded unexpectedly (contents: `%s`)", tt.contents) - } else { + cfg, err := getConfigFromContents(tt.contents, "auth") + if vErr := verifyFailure(tt.fail, tt.contents, err); vErr != nil { + t.Errorf("%v", vErr) + } else if !tt.fail { result := make(map[string]http.Header) for k, v := range cfg.AuthPerHost { result[k] = v.Header() @@ -104,6 +88,68 @@ func TestAuthConfigFormat(t *testing.T) { } } +func TestDockerAuthConfigFormat(t *testing.T) { + tests := []struct { + contents string + expected map[string]BasicCredentials + fail bool + }{ + {"bogus contents", nil, true}, + {`{"bogus": {"foo": "bar"}}`, nil, true}, + {`{"rktKind": "foo"}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "foo"}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1"}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1", "registries": "foo"}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1", "registries": []}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1", "registries": ["coreos.com"]}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1", "registries": ["coreos.com"], "credentials": {}}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1", "registries": ["coreos.com"], "credentials": {"user": ""}}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1", "registries": ["coreos.com"], "credentials": {"user": "bar"}}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1", "registries": ["coreos.com"], "credentials": {"user": "bar", "password": ""}}`, nil, true}, + {`{"rktKind": "dockerAuth", "rktVersion": "v1", "registries": ["coreos.com"], "credentials": {"user": "bar", "password": "baz"}}`, map[string]BasicCredentials{"coreos.com": BasicCredentials{User: "bar", Password: "baz"}}, false}, + } + for _, tt := range tests { + cfg, err := getConfigFromContents(tt.contents, "dockerAuth") + if vErr := verifyFailure(tt.fail, tt.contents, err); vErr != nil { + t.Errorf("%v", vErr) + } else if !tt.fail { + result := cfg.DockerCredentialsPerRegistry + if !reflect.DeepEqual(result, tt.expected) { + t.Error("Got unexpected results\nResult:\n", result, "\n\nExpected:\n", tt.expected) + } + } + } +} + +func verifyFailure(shouldFail bool, contents string, err error) error { + var vErr error = nil + if err != nil { + if !shouldFail { + vErr = fmt.Errorf("Expected test to succeed, failed unexpectedly (contents: `%s`): %v", contents, err) + } + } else if shouldFail { + vErr = fmt.Errorf("Expected test to fail, succeeded unexpectedly (contents: `%s`)", contents) + } + return vErr +} + +func getConfigFromContents(contents, kind string) (*Config, error) { + f, err := tmpConfigFile(tstprefix) + if err != nil { + panic(fmt.Sprintf("Failed to create tmp config file: %v", err)) + } + defer f.Close() + if _, err := f.Write([]byte(contents)); err != nil { + panic(fmt.Sprintf("Writing config to file failed: %v", err)) + } + fi, err := f.Stat() + if err != nil { + panic(fmt.Sprintf("Stating a tmp config file failed: %v", err)) + } + cfg := newConfig() + return cfg, readFile(cfg, fi, f.Name(), []string{kind}) +} + func TestConfigLoading(t *testing.T) { dir, err := ioutil.TempDir("", tstprefix) if err != nil { diff --git a/rkt/fetch.go b/rkt/fetch.go index c7c3b2f..f58604d 100644 --- a/rkt/fetch.go +++ b/rkt/fetch.go @@ -75,6 +75,7 @@ func runFetch(args []string) (exit int) { ds: ds, ks: ks, headers: config.AuthPerHost, + dockerAuth: config.DockerCredentialsPerRegistry, insecureSkipVerify: globalFlags.InsecureSkipVerify, debug: globalFlags.Debug, }, diff --git a/rkt/images.go b/rkt/images.go index f914232..6b5b1ff 100644 --- a/rkt/images.go +++ b/rkt/images.go @@ -43,6 +43,7 @@ type imageActionData struct { ds *store.Store ks *keystore.Keystore headers map[string]config.Headerer + dockerAuth map[string]config.BasicCredentials insecureSkipVerify bool debug bool } @@ -267,7 +268,14 @@ func (f *fetcher) fetch(aciURL, ascURL string, ascFile *os.File) (*openpgp.Entit return nil, nil, fmt.Errorf("error creating temporary dir for docker to ACI conversion: %v", err) } - acis, err := docker2aci.Convert(registryURL, true, tmpDir, "", "") + indexName := docker2aci.GetIndexName(registryURL) + user := "" + password := "" + if creds, ok := f.dockerAuth[indexName]; ok { + user = creds.User + password = creds.Password + } + acis, err := docker2aci.Convert(registryURL, true, tmpDir, user, password) if err != nil { return nil, nil, fmt.Errorf("error converting docker image to ACI: %v", err) } diff --git a/rkt/prepare.go b/rkt/prepare.go index 131439f..53e49bd 100644 --- a/rkt/prepare.go +++ b/rkt/prepare.go @@ -102,6 +102,7 @@ func runPrepare(args []string) (exit int) { imageActionData: imageActionData{ ds: ds, headers: config.AuthPerHost, + dockerAuth: config.DockerCredentialsPerRegistry, insecureSkipVerify: globalFlags.InsecureSkipVerify, debug: globalFlags.Debug, }, diff --git a/rkt/run.go b/rkt/run.go index acc48e3..9766bec 100644 --- a/rkt/run.go +++ b/rkt/run.go @@ -133,6 +133,7 @@ func runRun(args []string) (exit int) { imageActionData: imageActionData{ ds: ds, headers: config.AuthPerHost, + dockerAuth: config.DockerCredentialsPerRegistry, insecureSkipVerify: globalFlags.InsecureSkipVerify, debug: globalFlags.Debug, },