mirror of
https://github.com/clearlinux/rkt.git
synced 2026-08-19 05:27:23 +00:00
Merge pull request #767 from endocode/krnowak/docker-auth
Add config for docker auth
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user