Merge pull request #767 from endocode/krnowak/docker-auth

Add config for docker auth
This commit is contained in:
Krzesimir Nowak
2015-04-21 10:02:10 +02:00
8 changed files with 249 additions and 38 deletions
+107 -5
View File
@@ -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`.
+52 -10
View File
@@ -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
}
+12 -2
View File
@@ -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
}
}
+66 -20
View File
@@ -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 {
+1
View File
@@ -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,
},
+9 -1
View File
@@ -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)
}
+1
View File
@@ -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,
},
+1
View File
@@ -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,
},