vendor files

This commit is contained in:
Serguei Bezverkhi
2018-01-09 13:57:14 -05:00
parent 558bc6c02a
commit 7b24313bd6
16547 changed files with 4527373 additions and 0 deletions

56
vendor/k8s.io/kubernetes/pkg/credentialprovider/BUILD generated vendored Normal file
View File

@ -0,0 +1,56 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"config.go",
"doc.go",
"keyring.go",
"plugins.go",
"provider.go",
],
importpath = "k8s.io/kubernetes/pkg/credentialprovider",
deps = [
"//vendor/github.com/docker/docker/api/types:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"config_test.go",
"keyring_test.go",
"provider_test.go",
],
importpath = "k8s.io/kubernetes/pkg/credentialprovider",
library = ":go_default_library",
deps = ["//vendor/github.com/docker/docker/api/types:go_default_library"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/credentialprovider/aws:all-srcs",
"//pkg/credentialprovider/azure:all-srcs",
"//pkg/credentialprovider/gcp:all-srcs",
"//pkg/credentialprovider/rancher:all-srcs",
],
tags = ["automanaged"],
)

25
vendor/k8s.io/kubernetes/pkg/credentialprovider/OWNERS generated vendored Normal file
View File

@ -0,0 +1,25 @@
approvers:
- deads2k
- erictune
- liggitt
reviewers:
- thockin
- smarterclayton
- yujuhong
- brendandburns
- derekwaynecarr
- vishh
- mikedanese
- davidopp
- pmorie
- dchen1107
- justinsb
- eparis
- dims
- resouer
- mbohlool
- david-mcmahon
- mfojtik
- therc
- lixiaobing10051267
- goltermann

View File

@ -0,0 +1,46 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = ["aws_credentials.go"],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/aws",
deps = [
"//pkg/credentialprovider:go_default_library",
"//vendor/github.com/aws/aws-sdk-go/aws:go_default_library",
"//vendor/github.com/aws/aws-sdk-go/aws/request:go_default_library",
"//vendor/github.com/aws/aws-sdk-go/aws/session:go_default_library",
"//vendor/github.com/aws/aws-sdk-go/service/ecr:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["aws_credentials_test.go"],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/aws",
library = ":go_default_library",
deps = [
"//pkg/credentialprovider:go_default_library",
"//vendor/github.com/aws/aws-sdk-go/aws:go_default_library",
"//vendor/github.com/aws/aws-sdk-go/service/ecr:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

6
vendor/k8s.io/kubernetes/pkg/credentialprovider/aws/OWNERS generated vendored Executable file
View File

@ -0,0 +1,6 @@
reviewers:
- justinsb
- david-mcmahon
- therc
- lixiaobing10051267
- goltermann

View File

@ -0,0 +1,226 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentials
import (
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/credentialprovider"
)
const awsChinaRegionPrefix = "cn-"
const awsStandardDNSSuffix = "amazonaws.com"
const awsChinaDNSSuffix = "amazonaws.com.cn"
const registryURLTemplate = "*.dkr.ecr.%s.%s"
// awsHandlerLogger is a handler that logs all AWS SDK requests
// Copied from pkg/cloudprovider/providers/aws/log_handler.go
func awsHandlerLogger(req *request.Request) {
service := req.ClientInfo.ServiceName
region := req.Config.Region
name := "?"
if req.Operation != nil {
name = req.Operation.Name
}
glog.V(3).Infof("AWS request: %s:%s in %s", service, name, *region)
}
// An interface for testing purposes.
type tokenGetter interface {
GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error)
}
// The canonical implementation
type ecrTokenGetter struct {
svc *ecr.ECR
}
func (p *ecrTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
return p.svc.GetAuthorizationToken(input)
}
// lazyEcrProvider is a DockerConfigProvider that creates on demand an
// ecrProvider for a given region and then proxies requests to it.
type lazyEcrProvider struct {
region string
regionURL string
actualProvider *credentialprovider.CachingDockerConfigProvider
}
var _ credentialprovider.DockerConfigProvider = &lazyEcrProvider{}
// ecrProvider is a DockerConfigProvider that gets and refreshes 12-hour tokens
// from AWS to access ECR.
type ecrProvider struct {
region string
regionURL string
getter tokenGetter
}
var _ credentialprovider.DockerConfigProvider = &ecrProvider{}
// registryURL has different suffix in AWS China region
func registryURL(region string) string {
dnsSuffix := awsStandardDNSSuffix
// deal with aws none standard regions
if strings.HasPrefix(region, awsChinaRegionPrefix) {
dnsSuffix = awsChinaDNSSuffix
}
return fmt.Sprintf(registryURLTemplate, region, dnsSuffix)
}
// RegisterCredentialsProvider registers a credential provider for the specified region.
// It creates a lazy provider for each AWS region, in order to support
// cross-region ECR access. They have to be lazy because it's unlikely, but not
// impossible, that we'll use more than one.
// This should be called only if using the AWS cloud provider.
// This way, we avoid timeouts waiting for a non-existent provider.
func RegisterCredentialsProvider(region string) {
glog.V(4).Infof("registering credentials provider for AWS region %q", region)
credentialprovider.RegisterCredentialProvider("aws-ecr-"+region,
&lazyEcrProvider{
region: region,
regionURL: registryURL(region),
})
}
// Enabled implements DockerConfigProvider.Enabled for the lazy provider.
// Since we perform no checks/work of our own and actualProvider is only created
// later at image pulling time (if ever), always return true.
func (p *lazyEcrProvider) Enabled() bool {
return true
}
// LazyProvide implements DockerConfigProvider.LazyProvide. It will be called
// by the client when attempting to pull an image and it will create the actual
// provider only when we actually need it the first time.
func (p *lazyEcrProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
if p.actualProvider == nil {
glog.V(2).Infof("Creating ecrProvider for %s", p.region)
p.actualProvider = &credentialprovider.CachingDockerConfigProvider{
Provider: newEcrProvider(p.region, nil),
// Refresh credentials a little earlier than expiration time
Lifetime: 11*time.Hour + 55*time.Minute,
}
if !p.actualProvider.Enabled() {
return nil
}
}
entry := p.actualProvider.Provide()[p.regionURL]
return &entry
}
// Provide implements DockerConfigProvider.Provide, creating dummy credentials.
// Client code will call Provider.LazyProvide() at image pulling time.
func (p *lazyEcrProvider) Provide() credentialprovider.DockerConfig {
entry := credentialprovider.DockerConfigEntry{
Provider: p,
}
cfg := credentialprovider.DockerConfig{}
cfg[p.regionURL] = entry
return cfg
}
func newEcrProvider(region string, getter tokenGetter) *ecrProvider {
return &ecrProvider{
region: region,
regionURL: registryURL(region),
getter: getter,
}
}
// Enabled implements DockerConfigProvider.Enabled for the AWS token-based implementation.
// For now, it gets activated only if AWS was chosen as the cloud provider.
// TODO: figure how to enable it manually for deployments that are not on AWS but still
// use ECR somehow?
func (p *ecrProvider) Enabled() bool {
if p.region == "" {
glog.Errorf("Called ecrProvider.Enabled() with no region set")
return false
}
getter := &ecrTokenGetter{svc: ecr.New(session.New(&aws.Config{
Credentials: nil,
Region: &p.region,
}))}
getter.svc.Handlers.Sign.PushFrontNamed(request.NamedHandler{
Name: "k8s/logger",
Fn: awsHandlerLogger,
})
p.getter = getter
return true
}
// LazyProvide implements DockerConfigProvider.LazyProvide. Should never be called.
func (p *ecrProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
return nil
}
// Provide implements DockerConfigProvider.Provide, refreshing ECR tokens on demand
func (p *ecrProvider) Provide() credentialprovider.DockerConfig {
cfg := credentialprovider.DockerConfig{}
// TODO: fill in RegistryIds?
params := &ecr.GetAuthorizationTokenInput{}
output, err := p.getter.GetAuthorizationToken(params)
if err != nil {
glog.Errorf("while requesting ECR authorization token %v", err)
return cfg
}
if output == nil {
glog.Errorf("Got back no ECR token")
return cfg
}
for _, data := range output.AuthorizationData {
if data.ProxyEndpoint != nil &&
data.AuthorizationToken != nil {
decodedToken, err := base64.StdEncoding.DecodeString(aws.StringValue(data.AuthorizationToken))
if err != nil {
glog.Errorf("while decoding token for endpoint %v %v", data.ProxyEndpoint, err)
return cfg
}
parts := strings.SplitN(string(decodedToken), ":", 2)
user := parts[0]
password := parts[1]
entry := credentialprovider.DockerConfigEntry{
Username: user,
Password: password,
// ECR doesn't care and Docker is about to obsolete it
Email: "not@val.id",
}
glog.V(3).Infof("Adding credentials for user %s in %s", user, p.region)
// Add our config entry for this region's registry URLs
cfg[p.regionURL] = entry
}
}
return cfg
}

View File

@ -0,0 +1,163 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentials
import (
"encoding/base64"
"fmt"
"path"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecr"
"k8s.io/kubernetes/pkg/credentialprovider"
)
const user = "foo"
const password = "1234567890abcdef"
const email = "not@val.id"
// Mock implementation
type testTokenGetter struct {
user string
password string
endpoint string
}
func (p *testTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) {
expiration := time.Now().Add(1 * time.Hour)
creds := []byte(fmt.Sprintf("%s:%s", p.user, p.password))
data := &ecr.AuthorizationData{
AuthorizationToken: aws.String(base64.StdEncoding.EncodeToString(creds)),
ExpiresAt: &expiration,
ProxyEndpoint: aws.String(p.endpoint),
}
output := &ecr.GetAuthorizationTokenOutput{
AuthorizationData: []*ecr.AuthorizationData{data},
}
return output, nil //p.svc.GetAuthorizationToken(input)
}
func TestEcrProvide(t *testing.T) {
registry := "123456789012.dkr.ecr.lala-land-1.amazonaws.com"
otherRegistries := []string{
"123456789012.dkr.ecr.cn-foo-1.amazonaws.com.cn",
"private.registry.com",
"gcr.io",
}
image := "foo/bar"
provider := newEcrProvider("lala-land-1",
&testTokenGetter{
user: user,
password: password,
endpoint: registry,
})
keyring := &credentialprovider.BasicDockerKeyring{}
keyring.Add(provider.Provide())
// Verify that we get the expected username/password combo for
// an ECR image name.
fullImage := path.Join(registry, image)
creds, ok := keyring.Lookup(fullImage)
if !ok {
t.Errorf("Didn't find expected URL: %s", fullImage)
return
}
if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if user != val.Username {
t.Errorf("Unexpected username value, want: _token, got: %s", val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
// Verify that we get an error for other images.
for _, otherRegistry := range otherRegistries {
fullImage = path.Join(otherRegistry, image)
creds, ok = keyring.Lookup(fullImage)
if ok {
t.Errorf("Unexpectedly found image: %s", fullImage)
return
}
}
}
func TestChinaEcrProvide(t *testing.T) {
registry := "123456789012.dkr.ecr.cn-foo-1.amazonaws.com.cn"
otherRegistries := []string{
"123456789012.dkr.ecr.lala-land-1.amazonaws.com",
"private.registry.com",
"gcr.io",
}
image := "foo/bar"
provider := newEcrProvider("cn-foo-1",
&testTokenGetter{
user: user,
password: password,
endpoint: registry,
})
keyring := &credentialprovider.BasicDockerKeyring{}
keyring.Add(provider.Provide())
// Verify that we get the expected username/password combo for
// an ECR image name.
fullImage := path.Join(registry, image)
creds, ok := keyring.Lookup(fullImage)
if !ok {
t.Errorf("Didn't find expected URL: %s", fullImage)
return
}
if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if user != val.Username {
t.Errorf("Unexpected username value, want: _token, got: %s", val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
// Verify that we get an error for other images.
for _, otherRegistry := range otherRegistries {
fullImage = path.Join(otherRegistry, image)
creds, ok = keyring.Lookup(fullImage)
if ok {
t.Errorf("Unexpectedly found image: %s", fullImage)
return
}
}
}

View File

@ -0,0 +1,51 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"azure_acr_helper.go",
"azure_credentials.go",
],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/azure",
deps = [
"//pkg/cloudprovider/providers/azure:go_default_library",
"//pkg/credentialprovider:go_default_library",
"//vendor/github.com/Azure/azure-sdk-for-go/arm/containerregistry:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest/adal:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest/azure:go_default_library",
"//vendor/github.com/dgrijalva/jwt-go:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["azure_credentials_test.go"],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/azure",
library = ":go_default_library",
deps = [
"//vendor/github.com/Azure/azure-sdk-for-go/arm/containerregistry:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest/to:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,300 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Copyright 2017 Microsoft Corporation
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
*/
// Source: https://github.com/Azure/acr-docker-credential-helper/blob/a79b541f3ee761f6cc4511863ed41fb038c19464/src/docker-credential-acr/acr_login.go
package azure
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"unicode"
jwt "github.com/dgrijalva/jwt-go"
)
type authDirective struct {
service string
realm string
}
type accessTokenPayload struct {
TenantID string `json:"tid"`
}
type acrTokenPayload struct {
Expiration int64 `json:"exp"`
TenantID string `json:"tenant"`
Credential string `json:"credential"`
}
type acrAuthResponse struct {
RefreshToken string `json:"refresh_token"`
}
// 5 minutes buffer time to allow timeshift between local machine and AAD
const timeShiftBuffer = 300
const userAgentHeader = "User-Agent"
const userAgent = "kubernetes-credentialprovider-acr"
const dockerTokenLoginUsernameGUID = "00000000-0000-0000-0000-000000000000"
var client = &http.Client{}
func receiveChallengeFromLoginServer(serverAddress string) (*authDirective, error) {
challengeURL := url.URL{
Scheme: "https",
Host: serverAddress,
Path: "v2/",
}
var err error
var r *http.Request
r, _ = http.NewRequest("GET", challengeURL.String(), nil)
r.Header.Add(userAgentHeader, userAgent)
var challenge *http.Response
if challenge, err = client.Do(r); err != nil {
return nil, fmt.Errorf("Error reaching registry endpoint %s, error: %s", challengeURL.String(), err)
}
defer challenge.Body.Close()
if challenge.StatusCode != 401 {
return nil, fmt.Errorf("Registry did not issue a valid AAD challenge, status: %d", challenge.StatusCode)
}
var authHeader []string
var ok bool
if authHeader, ok = challenge.Header["Www-Authenticate"]; !ok {
return nil, fmt.Errorf("Challenge response does not contain header 'Www-Authenticate'")
}
if len(authHeader) != 1 {
return nil, fmt.Errorf("Registry did not issue a valid AAD challenge, authenticate header [%s]",
strings.Join(authHeader, ", "))
}
authSections := strings.SplitN(authHeader[0], " ", 2)
authType := strings.ToLower(authSections[0])
var authParams *map[string]string
if authParams, err = parseAssignments(authSections[1]); err != nil {
return nil, fmt.Errorf("Unable to understand the contents of Www-Authenticate header %s", authSections[1])
}
// verify headers
if !strings.EqualFold("Bearer", authType) {
return nil, fmt.Errorf("Www-Authenticate: expected realm: Bearer, actual: %s", authType)
}
if len((*authParams)["service"]) == 0 {
return nil, fmt.Errorf("Www-Authenticate: missing header \"service\"")
}
if len((*authParams)["realm"]) == 0 {
return nil, fmt.Errorf("Www-Authenticate: missing header \"realm\"")
}
return &authDirective{
service: (*authParams)["service"],
realm: (*authParams)["realm"],
}, nil
}
func parseAcrToken(identityToken string) (token *acrTokenPayload, err error) {
tokenSegments := strings.Split(identityToken, ".")
if len(tokenSegments) < 2 {
return nil, fmt.Errorf("Invalid existing refresh token length: %d", len(tokenSegments))
}
payloadSegmentEncoded := tokenSegments[1]
var payloadBytes []byte
if payloadBytes, err = jwt.DecodeSegment(payloadSegmentEncoded); err != nil {
return nil, fmt.Errorf("Error decoding payload segment from refresh token, error: %s", err)
}
var payload acrTokenPayload
if err = json.Unmarshal(payloadBytes, &payload); err != nil {
return nil, fmt.Errorf("Error unmarshalling acr payload, error: %s", err)
}
return &payload, nil
}
func performTokenExchange(
serverAddress string,
directive *authDirective,
tenant string,
accessToken string) (string, error) {
var err error
data := url.Values{
"service": []string{directive.service},
"grant_type": []string{"access_token_refresh_token"},
"access_token": []string{accessToken},
"refresh_token": []string{accessToken},
"tenant": []string{tenant},
}
var realmURL *url.URL
if realmURL, err = url.Parse(directive.realm); err != nil {
return "", fmt.Errorf("Www-Authenticate: invalid realm %s", directive.realm)
}
authEndpoint := fmt.Sprintf("%s://%s/oauth2/exchange", realmURL.Scheme, realmURL.Host)
datac := data.Encode()
var r *http.Request
r, _ = http.NewRequest("POST", authEndpoint, bytes.NewBufferString(datac))
r.Header.Add(userAgentHeader, userAgent)
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
r.Header.Add("Content-Length", strconv.Itoa(len(datac)))
var exchange *http.Response
if exchange, err = client.Do(r); err != nil {
return "", fmt.Errorf("Www-Authenticate: failed to reach auth url %s", authEndpoint)
}
defer exchange.Body.Close()
if exchange.StatusCode != 200 {
return "", fmt.Errorf("Www-Authenticate: auth url %s responded with status code %d", authEndpoint, exchange.StatusCode)
}
var content []byte
if content, err = ioutil.ReadAll(exchange.Body); err != nil {
return "", fmt.Errorf("Www-Authenticate: error reading response from %s", authEndpoint)
}
var authResp acrAuthResponse
if err = json.Unmarshal(content, &authResp); err != nil {
return "", fmt.Errorf("Www-Authenticate: unable to read response %s", content)
}
return authResp.RefreshToken, nil
}
// Try and parse a string of assignments in the form of:
// key1 = value1, key2 = "value 2", key3 = ""
// Note: this method and handle quotes but does not handle escaping of quotes
func parseAssignments(statements string) (*map[string]string, error) {
var cursor int
result := make(map[string]string)
var errorMsg = fmt.Errorf("malformed header value: %s", statements)
for {
// parse key
equalIndex := nextOccurrence(statements, cursor, "=")
if equalIndex == -1 {
return nil, errorMsg
}
key := strings.TrimSpace(statements[cursor:equalIndex])
// parse value
cursor = nextNoneSpace(statements, equalIndex+1)
if cursor == -1 {
return nil, errorMsg
}
// case: value is quoted
if statements[cursor] == '"' {
cursor = cursor + 1
// like I said, not handling escapes, but this will skip any comma that's
// within the quotes which is somewhat more likely
closeQuoteIndex := nextOccurrence(statements, cursor, "\"")
if closeQuoteIndex == -1 {
return nil, errorMsg
}
value := statements[cursor:closeQuoteIndex]
result[key] = value
commaIndex := nextNoneSpace(statements, closeQuoteIndex+1)
if commaIndex == -1 {
// no more comma, done
return &result, nil
} else if statements[commaIndex] != ',' {
// expect comma immediately after close quote
return nil, errorMsg
} else {
cursor = commaIndex + 1
}
} else {
commaIndex := nextOccurrence(statements, cursor, ",")
endStatements := commaIndex == -1
var untrimmed string
if endStatements {
untrimmed = statements[cursor:commaIndex]
} else {
untrimmed = statements[cursor:]
}
value := strings.TrimSpace(untrimmed)
if len(value) == 0 {
// disallow empty value without quote
return nil, errorMsg
}
result[key] = value
if endStatements {
return &result, nil
}
cursor = commaIndex + 1
}
}
}
func nextOccurrence(str string, start int, sep string) int {
if start >= len(str) {
return -1
}
offset := strings.Index(str[start:], sep)
if offset == -1 {
return -1
}
return offset + start
}
func nextNoneSpace(str string, start int) int {
if start >= len(str) {
return -1
}
offset := strings.IndexFunc(str[start:], func(c rune) bool { return !unicode.IsSpace(c) })
if offset == -1 {
return -1
}
return offset + start
}

View File

@ -0,0 +1,175 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package azure
import (
"io"
"os"
"time"
"github.com/Azure/azure-sdk-for-go/arm/containerregistry"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
azureapi "github.com/Azure/go-autorest/autorest/azure"
"github.com/golang/glog"
"github.com/spf13/pflag"
"k8s.io/kubernetes/pkg/cloudprovider/providers/azure"
"k8s.io/kubernetes/pkg/credentialprovider"
)
var flagConfigFile = pflag.String("azure-container-registry-config", "",
"Path to the file container Azure container registry configuration information.")
const dummyRegistryEmail = "name@contoso.com"
// init registers the various means by which credentials may
// be resolved on Azure.
func init() {
credentialprovider.RegisterCredentialProvider("azure",
&credentialprovider.CachingDockerConfigProvider{
Provider: NewACRProvider(flagConfigFile),
Lifetime: 1 * time.Minute,
})
}
// RegistriesClient is a testable interface for the ACR client List operation.
type RegistriesClient interface {
List() (containerregistry.RegistryListResult, error)
}
// NewACRProvider parses the specified configFile and returns a DockerConfigProvider
func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider {
return &acrProvider{
file: configFile,
}
}
type acrProvider struct {
file *string
config *azure.Config
environment *azureapi.Environment
registryClient RegistriesClient
servicePrincipalToken *adal.ServicePrincipalToken
}
func (a *acrProvider) loadConfig(rdr io.Reader) error {
var err error
a.config, a.environment, err = azure.ParseConfig(rdr)
if err != nil {
glog.Errorf("Failed to load azure credential file: %v", err)
}
return nil
}
func (a *acrProvider) Enabled() bool {
if a.file == nil || len(*a.file) == 0 {
glog.V(5).Infof("Azure config unspecified, disabling")
return false
}
f, err := os.Open(*a.file)
if err != nil {
glog.Errorf("Failed to load config from file: %s", *a.file)
return false
}
defer f.Close()
err = a.loadConfig(f)
if err != nil {
glog.Errorf("Failed to load config from file: %s", *a.file)
return false
}
a.servicePrincipalToken, err = azure.GetServicePrincipalToken(a.config, a.environment)
if err != nil {
glog.Errorf("Failed to create service principal token: %v", err)
return false
}
registryClient := containerregistry.NewRegistriesClient(a.config.SubscriptionID)
registryClient.BaseURI = a.environment.ResourceManagerEndpoint
registryClient.Authorizer = autorest.NewBearerAuthorizer(a.servicePrincipalToken)
a.registryClient = registryClient
return true
}
func (a *acrProvider) Provide() credentialprovider.DockerConfig {
cfg := credentialprovider.DockerConfig{}
glog.V(4).Infof("listing registries")
res, err := a.registryClient.List()
if err != nil {
glog.Errorf("Failed to list registries: %v", err)
return cfg
}
for ix := range *res.Value {
loginServer := getLoginServer((*res.Value)[ix])
var cred *credentialprovider.DockerConfigEntry
if a.config.UseManagedIdentityExtension {
cred, err = getACRDockerEntryFromARMToken(a, loginServer)
if err != nil {
continue
}
} else {
cred = &credentialprovider.DockerConfigEntry{
Username: a.config.AADClientID,
Password: a.config.AADClientSecret,
Email: dummyRegistryEmail,
}
}
cfg[loginServer] = *cred
}
return cfg
}
func getLoginServer(registry containerregistry.Registry) string {
return *(*registry.RegistryProperties).LoginServer
}
func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialprovider.DockerConfigEntry, error) {
armAccessToken := a.servicePrincipalToken.AccessToken
glog.V(4).Infof("discovering auth redirects for: %s", loginServer)
directive, err := receiveChallengeFromLoginServer(loginServer)
if err != nil {
glog.Errorf("failed to receive challenge: %s", err)
return nil, err
}
glog.V(4).Infof("exchanging an acr refresh_token")
registryRefreshToken, err := performTokenExchange(
loginServer, directive, a.config.TenantID, armAccessToken)
if err != nil {
glog.Errorf("failed to perform token exchange: %s", err)
return nil, err
}
glog.V(4).Infof("adding ACR docker config entry for: %s", loginServer)
return &credentialprovider.DockerConfigEntry{
Username: dockerTokenLoginUsernameGUID,
Password: registryRefreshToken,
Email: dummyRegistryEmail,
}, nil
}
func (a *acrProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
return nil
}

View File

@ -0,0 +1,91 @@
/*
Copyright 2016 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package azure
import (
"bytes"
"testing"
"github.com/Azure/azure-sdk-for-go/arm/containerregistry"
"github.com/Azure/go-autorest/autorest/to"
)
type fakeClient struct {
results containerregistry.RegistryListResult
}
func (f *fakeClient) List() (containerregistry.RegistryListResult, error) {
return f.results, nil
}
func Test(t *testing.T) {
configStr := `
{
"aadClientId": "foo",
"aadClientSecret": "bar"
}`
result := containerregistry.RegistryListResult{
Value: &[]containerregistry.Registry{
{
Name: to.StringPtr("foo"),
RegistryProperties: &containerregistry.RegistryProperties{
LoginServer: to.StringPtr("foo-microsoft.azurecr.io"),
},
},
{
Name: to.StringPtr("bar"),
RegistryProperties: &containerregistry.RegistryProperties{
LoginServer: to.StringPtr("bar-microsoft.azurecr.io"),
},
},
{
Name: to.StringPtr("baz"),
RegistryProperties: &containerregistry.RegistryProperties{
LoginServer: to.StringPtr("baz-microsoft.azurecr.io"),
},
},
},
}
fakeClient := &fakeClient{
results: result,
}
provider := &acrProvider{
registryClient: fakeClient,
}
provider.loadConfig(bytes.NewBufferString(configStr))
creds := provider.Provide()
if len(creds) != len(*result.Value) {
t.Errorf("Unexpected list: %v, expected length %d", creds, len(*result.Value))
}
for _, cred := range creds {
if cred.Username != "foo" {
t.Errorf("expected 'foo' for username, saw: %v", cred.Username)
}
if cred.Password != "bar" {
t.Errorf("expected 'bar' for password, saw: %v", cred.Username)
}
}
for _, val := range *result.Value {
registryName := getLoginServer(val)
if _, found := creds[registryName]; !found {
t.Errorf("Missing expected registry: %s", registryName)
}
}
}

View File

@ -0,0 +1,295 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/golang/glog"
)
// DockerConfigJson represents ~/.docker/config.json file info
// see https://github.com/docker/docker/pull/12009
type DockerConfigJson struct {
Auths DockerConfig `json:"auths"`
// +optional
HttpHeaders map[string]string `json:"HttpHeaders,omitempty"`
}
// DockerConfig represents the config file used by the docker CLI.
// This config that represents the credentials that should be used
// when pulling images from specific image repositories.
type DockerConfig map[string]DockerConfigEntry
type DockerConfigEntry struct {
Username string
Password string
Email string
Provider DockerConfigProvider
}
var (
preferredPathLock sync.Mutex
preferredPath = ""
workingDirPath = ""
homeDirPath = os.Getenv("HOME")
rootDirPath = "/"
homeJsonDirPath = filepath.Join(homeDirPath, ".docker")
rootJsonDirPath = filepath.Join(rootDirPath, ".docker")
configFileName = ".dockercfg"
configJsonFileName = "config.json"
)
func SetPreferredDockercfgPath(path string) {
preferredPathLock.Lock()
defer preferredPathLock.Unlock()
preferredPath = path
}
func GetPreferredDockercfgPath() string {
preferredPathLock.Lock()
defer preferredPathLock.Unlock()
return preferredPath
}
//DefaultDockercfgPaths returns default search paths of .dockercfg
func DefaultDockercfgPaths() []string {
return []string{GetPreferredDockercfgPath(), workingDirPath, homeDirPath, rootDirPath}
}
//DefaultDockerConfigJSONPaths returns default search paths of .docker/config.json
func DefaultDockerConfigJSONPaths() []string {
return []string{GetPreferredDockercfgPath(), workingDirPath, homeJsonDirPath, rootJsonDirPath}
}
// ReadDockercfgFile attempts to read a legacy dockercfg file from the given paths.
// if searchPaths is empty, the default paths are used.
func ReadDockercfgFile(searchPaths []string) (cfg DockerConfig, err error) {
if len(searchPaths) == 0 {
searchPaths = DefaultDockercfgPaths()
}
for _, configPath := range searchPaths {
absDockerConfigFileLocation, err := filepath.Abs(filepath.Join(configPath, configFileName))
if err != nil {
glog.Errorf("while trying to canonicalize %s: %v", configPath, err)
continue
}
glog.V(4).Infof("looking for .dockercfg at %s", absDockerConfigFileLocation)
contents, err := ioutil.ReadFile(absDockerConfigFileLocation)
if os.IsNotExist(err) {
continue
}
if err != nil {
glog.V(4).Infof("while trying to read %s: %v", absDockerConfigFileLocation, err)
continue
}
cfg, err := readDockerConfigFileFromBytes(contents)
if err == nil {
glog.V(4).Infof("found .dockercfg at %s", absDockerConfigFileLocation)
return cfg, nil
}
}
return nil, fmt.Errorf("couldn't find valid .dockercfg after checking in %v", searchPaths)
}
// ReadDockerConfigJSONFile attempts to read a docker config.json file from the given paths.
// if searchPaths is empty, the default paths are used.
func ReadDockerConfigJSONFile(searchPaths []string) (cfg DockerConfig, err error) {
if len(searchPaths) == 0 {
searchPaths = DefaultDockerConfigJSONPaths()
}
for _, configPath := range searchPaths {
absDockerConfigFileLocation, err := filepath.Abs(filepath.Join(configPath, configJsonFileName))
if err != nil {
glog.Errorf("while trying to canonicalize %s: %v", configPath, err)
continue
}
glog.V(4).Infof("looking for %s at %s", configJsonFileName, absDockerConfigFileLocation)
cfg, err = ReadSpecificDockerConfigJsonFile(absDockerConfigFileLocation)
if err != nil {
if !os.IsNotExist(err) {
glog.V(4).Infof("while trying to read %s: %v", absDockerConfigFileLocation, err)
}
continue
}
glog.V(4).Infof("found valid %s at %s", configJsonFileName, absDockerConfigFileLocation)
return cfg, nil
}
return nil, fmt.Errorf("couldn't find valid %s after checking in %v", configJsonFileName, searchPaths)
}
//ReadSpecificDockerConfigJsonFile attempts to read docker configJSON from a given file path.
func ReadSpecificDockerConfigJsonFile(filePath string) (cfg DockerConfig, err error) {
var contents []byte
if contents, err = ioutil.ReadFile(filePath); err != nil {
return nil, err
}
return readDockerConfigJsonFileFromBytes(contents)
}
func ReadDockerConfigFile() (cfg DockerConfig, err error) {
if cfg, err := ReadDockerConfigJSONFile(nil); err == nil {
return cfg, nil
}
// Can't find latest config file so check for the old one
return ReadDockercfgFile(nil)
}
// HttpError wraps a non-StatusOK error code as an error.
type HttpError struct {
StatusCode int
Url string
}
// Error implements error
func (he *HttpError) Error() string {
return fmt.Sprintf("http status code: %d while fetching url %s",
he.StatusCode, he.Url)
}
func ReadUrl(url string, client *http.Client, header *http.Header) (body []byte, err error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if header != nil {
req.Header = *header
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
glog.V(2).Infof("body of failing http response: %v", resp.Body)
return nil, &HttpError{
StatusCode: resp.StatusCode,
Url: url,
}
}
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return contents, nil
}
func ReadDockerConfigFileFromUrl(url string, client *http.Client, header *http.Header) (cfg DockerConfig, err error) {
if contents, err := ReadUrl(url, client, header); err != nil {
return nil, err
} else {
return readDockerConfigFileFromBytes(contents)
}
}
func readDockerConfigFileFromBytes(contents []byte) (cfg DockerConfig, err error) {
if err = json.Unmarshal(contents, &cfg); err != nil {
glog.Errorf("while trying to parse blob %q: %v", contents, err)
return nil, err
}
return
}
func readDockerConfigJsonFileFromBytes(contents []byte) (cfg DockerConfig, err error) {
var cfgJson DockerConfigJson
if err = json.Unmarshal(contents, &cfgJson); err != nil {
glog.Errorf("while trying to parse blob %q: %v", contents, err)
return nil, err
}
cfg = cfgJson.Auths
return
}
// dockerConfigEntryWithAuth is used solely for deserializing the Auth field
// into a dockerConfigEntry during JSON deserialization.
type dockerConfigEntryWithAuth struct {
// +optional
Username string `json:"username,omitempty"`
// +optional
Password string `json:"password,omitempty"`
// +optional
Email string `json:"email,omitempty"`
// +optional
Auth string `json:"auth,omitempty"`
}
func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error {
var tmp dockerConfigEntryWithAuth
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
ident.Username = tmp.Username
ident.Password = tmp.Password
ident.Email = tmp.Email
if len(tmp.Auth) == 0 {
return nil
}
ident.Username, ident.Password, err = decodeDockerConfigFieldAuth(tmp.Auth)
return err
}
func (ident DockerConfigEntry) MarshalJSON() ([]byte, error) {
toEncode := dockerConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""}
toEncode.Auth = encodeDockerConfigFieldAuth(ident.Username, ident.Password)
return json.Marshal(toEncode)
}
// decodeDockerConfigFieldAuth deserializes the "auth" field from dockercfg into a
// username and a password. The format of the auth field is base64(<username>:<password>).
func decodeDockerConfigFieldAuth(field string) (username, password string, err error) {
decoded, err := base64.StdEncoding.DecodeString(field)
if err != nil {
return
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
err = fmt.Errorf("unable to parse auth field")
return
}
username = parts[0]
password = parts[1]
return
}
func encodeDockerConfigFieldAuth(username, password string) string {
fieldValue := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(fieldValue))
}

View File

@ -0,0 +1,266 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
func TestReadDockerConfigFile(t *testing.T) {
configJsonFileName := "config.json"
var fileInfo *os.File
preferredPaths := []string{}
//test dockerconfig json
inputDockerconfigJsonFile := "{ \"auths\": { \"http://foo.example.com\":{\"auth\":\"Zm9vOmJhcgo=\",\"email\":\"foo@example.com\"}}}"
preferredPath, err := ioutil.TempDir("", "test_foo_bar_dockerconfigjson_")
if err != nil {
t.Fatalf("Creating tmp dir fail: %v", err)
return
}
defer os.RemoveAll(preferredPath)
preferredPaths = append(preferredPaths, preferredPath)
absDockerConfigFileLocation, err := filepath.Abs(filepath.Join(preferredPath, configJsonFileName))
if err != nil {
t.Fatalf("While trying to canonicalize %s: %v", preferredPath, err)
}
if _, err := os.Stat(absDockerConfigFileLocation); os.IsNotExist(err) {
//create test cfg file
fileInfo, err = os.OpenFile(absDockerConfigFileLocation, os.O_CREATE|os.O_RDWR, 0664)
if err != nil {
t.Fatalf("While trying to create file %s: %v", absDockerConfigFileLocation, err)
}
defer fileInfo.Close()
}
fileInfo.WriteString(inputDockerconfigJsonFile)
orgPreferredPath := GetPreferredDockercfgPath()
SetPreferredDockercfgPath(preferredPath)
defer SetPreferredDockercfgPath(orgPreferredPath)
if _, err := ReadDockerConfigFile(); err != nil {
t.Errorf("Getting docker config file fail : %v preferredPath : %q", err, preferredPath)
}
}
func TestDockerConfigJsonJSONDecode(t *testing.T) {
input := []byte(`{"auths": {"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "bar@example.com"}}}`)
expect := DockerConfigJson{
Auths: DockerConfig(map[string]DockerConfigEntry{
"http://foo.example.com": {
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
"http://bar.example.com": {
Username: "bar",
Password: "baz",
Email: "bar@example.com",
},
}),
}
var output DockerConfigJson
err := json.Unmarshal(input, &output)
if err != nil {
t.Errorf("Received unexpected error: %v", err)
}
if !reflect.DeepEqual(expect, output) {
t.Errorf("Received unexpected output. Expected %#v, got %#v", expect, output)
}
}
func TestDockerConfigJSONDecode(t *testing.T) {
input := []byte(`{"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "bar@example.com"}}`)
expect := DockerConfig(map[string]DockerConfigEntry{
"http://foo.example.com": {
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
"http://bar.example.com": {
Username: "bar",
Password: "baz",
Email: "bar@example.com",
},
})
var output DockerConfig
err := json.Unmarshal(input, &output)
if err != nil {
t.Errorf("Received unexpected error: %v", err)
}
if !reflect.DeepEqual(expect, output) {
t.Errorf("Received unexpected output. Expected %#v, got %#v", expect, output)
}
}
func TestDockerConfigEntryJSONDecode(t *testing.T) {
tests := []struct {
input []byte
expect DockerConfigEntry
fail bool
}{
// simple case, just decode the fields
{
input: []byte(`{"username": "foo", "password": "bar", "email": "foo@example.com"}`),
expect: DockerConfigEntry{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
fail: false,
},
// auth field decodes to username & password
{
input: []byte(`{"auth": "Zm9vOmJhcg==", "email": "foo@example.com"}`),
expect: DockerConfigEntry{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
fail: false,
},
// auth field overrides username & password
{
input: []byte(`{"username": "foo", "password": "bar", "auth": "cGluZzpwb25n", "email": "foo@example.com"}`),
expect: DockerConfigEntry{
Username: "ping",
Password: "pong",
Email: "foo@example.com",
},
fail: false,
},
// poorly-formatted auth causes failure
{
input: []byte(`{"auth": "pants", "email": "foo@example.com"}`),
expect: DockerConfigEntry{
Username: "",
Password: "",
Email: "foo@example.com",
},
fail: true,
},
// invalid JSON causes failure
{
input: []byte(`{"email": false}`),
expect: DockerConfigEntry{
Username: "",
Password: "",
Email: "",
},
fail: true,
},
}
for i, tt := range tests {
var output DockerConfigEntry
err := json.Unmarshal(tt.input, &output)
if (err != nil) != tt.fail {
t.Errorf("case %d: expected fail=%t, got err=%v", i, tt.fail, err)
}
if !reflect.DeepEqual(tt.expect, output) {
t.Errorf("case %d: expected output %#v, got %#v", i, tt.expect, output)
}
}
}
func TestDecodeDockerConfigFieldAuth(t *testing.T) {
tests := []struct {
input string
username string
password string
fail bool
}{
// auth field decodes to username & password
{
input: "Zm9vOmJhcg==",
username: "foo",
password: "bar",
},
// good base64 data, but no colon separating username & password
{
input: "cGFudHM=",
fail: true,
},
// bad base64 data
{
input: "pants",
fail: true,
},
}
for i, tt := range tests {
username, password, err := decodeDockerConfigFieldAuth(tt.input)
if (err != nil) != tt.fail {
t.Errorf("case %d: expected fail=%t, got err=%v", i, tt.fail, err)
}
if tt.username != username {
t.Errorf("case %d: expected username %q, got %q", i, tt.username, username)
}
if tt.password != password {
t.Errorf("case %d: expected password %q, got %q", i, tt.password, password)
}
}
}
func TestDockerConfigEntryJSONCompatibleEncode(t *testing.T) {
tests := []struct {
input DockerConfigEntry
expect []byte
}{
// simple case, just decode the fields
{
expect: []byte(`{"username":"foo","password":"bar","email":"foo@example.com","auth":"Zm9vOmJhcg=="}`),
input: DockerConfigEntry{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
},
}
for i, tt := range tests {
actual, err := json.Marshal(tt.input)
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
}
if string(tt.expect) != string(actual) {
t.Errorf("case %d: expected %v, got %v", i, string(tt.expect), string(actual))
}
}
}

19
vendor/k8s.io/kubernetes/pkg/credentialprovider/doc.go generated vendored Normal file
View File

@ -0,0 +1,19 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package credentialprovider supplies interfaces and implementations for
// docker registry providers to expose their authentication scheme.
package credentialprovider // import "k8s.io/kubernetes/pkg/credentialprovider"

View File

@ -0,0 +1,53 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"jwt.go",
"metadata.go",
],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/gcp",
deps = [
"//pkg/credentialprovider:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/spf13/pflag:go_default_library",
"//vendor/golang.org/x/oauth2:go_default_library",
"//vendor/golang.org/x/oauth2/google:go_default_library",
"//vendor/golang.org/x/oauth2/jwt:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = [
"jwt_test.go",
"metadata_test.go",
],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/gcp",
library = ":go_default_library",
deps = [
"//pkg/credentialprovider:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

12
vendor/k8s.io/kubernetes/pkg/credentialprovider/gcp/OWNERS generated vendored Executable file
View File

@ -0,0 +1,12 @@
reviewers:
- thockin
- deads2k
- yujuhong
- derekwaynecarr
- vishh
- mikedanese
- davidopp
- eparis
- dims
- david-mcmahon
- therc

View File

@ -0,0 +1,19 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package gcp_credentials contains implementations of DockerConfigProvider
// for Google Cloud Platform.
package gcp_credentials // import "k8s.io/kubernetes/pkg/credentialprovider/gcp"

View File

@ -0,0 +1,116 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gcp_credentials
import (
"io/ioutil"
"time"
"github.com/golang/glog"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/jwt"
"k8s.io/kubernetes/pkg/credentialprovider"
"github.com/spf13/pflag"
)
const (
storageReadOnlyScope = "https://www.googleapis.com/auth/devstorage.read_only"
)
var (
flagJwtFile = pflag.String("google-json-key", "",
"The Google Cloud Platform Service Account JSON Key to use for authentication.")
)
// A DockerConfigProvider that reads its configuration from Google
// Compute Engine metadata.
type jwtProvider struct {
path *string
config *jwt.Config
tokenUrl string
}
// init registers the various means by which credentials may
// be resolved on GCP.
func init() {
credentialprovider.RegisterCredentialProvider("google-jwt-key",
&credentialprovider.CachingDockerConfigProvider{
Provider: &jwtProvider{
path: flagJwtFile,
},
Lifetime: 30 * time.Minute,
})
}
// Enabled implements DockerConfigProvider for the JSON Key based implementation.
func (j *jwtProvider) Enabled() bool {
if *j.path == "" {
return false
}
data, err := ioutil.ReadFile(*j.path)
if err != nil {
glog.Errorf("while reading file %s got %v", *j.path, err)
return false
}
config, err := google.JWTConfigFromJSON(data, storageReadOnlyScope)
if err != nil {
glog.Errorf("while parsing %s data got %v", *j.path, err)
return false
}
j.config = config
if j.tokenUrl != "" {
j.config.TokenURL = j.tokenUrl
}
return true
}
// LazyProvide implements DockerConfigProvider. Should never be called.
func (j *jwtProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
return nil
}
// Provide implements DockerConfigProvider
func (j *jwtProvider) Provide() credentialprovider.DockerConfig {
cfg := credentialprovider.DockerConfig{}
ts := j.config.TokenSource(oauth2.NoContext)
token, err := ts.Token()
if err != nil {
glog.Errorf("while exchanging json key %s for access token %v", *j.path, err)
return cfg
}
if !token.Valid() {
glog.Errorf("Got back invalid token: %v", token)
return cfg
}
entry := credentialprovider.DockerConfigEntry{
Username: "_token",
Password: token.AccessToken,
Email: j.config.Email,
}
// Add our entry for each of the supported container registry URLs
for _, k := range containerRegistryUrls {
cfg[k] = entry
}
return cfg
}

View File

@ -0,0 +1,127 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gcp_credentials
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"k8s.io/kubernetes/pkg/credentialprovider"
)
const email = "foo@bar.com"
// From oauth2/jwt_test.go
var (
dummyPrivateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAx4fm7dngEmOULNmAs1IGZ9Apfzh+BkaQ1dzkmbUgpcoghucE
DZRnAGd2aPyB6skGMXUytWQvNYav0WTR00wFtX1ohWTfv68HGXJ8QXCpyoSKSSFY
fuP9X36wBSkSX9J5DVgiuzD5VBdzUISSmapjKm+DcbRALjz6OUIPEWi1Tjl6p5RK
1w41qdbmt7E5/kGhKLDuT7+M83g4VWhgIvaAXtnhklDAggilPPa8ZJ1IFe31lNlr
k4DRk38nc6sEutdf3RL7QoH7FBusI7uXV03DC6dwN1kP4GE7bjJhcRb/7jYt7CQ9
/E9Exz3c0yAp0yrTg0Fwh+qxfH9dKwN52S7SBwIDAQABAoIBAQCaCs26K07WY5Jt
3a2Cw3y2gPrIgTCqX6hJs7O5ByEhXZ8nBwsWANBUe4vrGaajQHdLj5OKfsIDrOvn
2NI1MqflqeAbu/kR32q3tq8/Rl+PPiwUsW3E6Pcf1orGMSNCXxeducF2iySySzh3
nSIhCG5uwJDWI7a4+9KiieFgK1pt/Iv30q1SQS8IEntTfXYwANQrfKUVMmVF9aIK
6/WZE2yd5+q3wVVIJ6jsmTzoDCX6QQkkJICIYwCkglmVy5AeTckOVwcXL0jqw5Kf
5/soZJQwLEyBoQq7Kbpa26QHq+CJONetPP8Ssy8MJJXBT+u/bSseMb3Zsr5cr43e
DJOhwsThAoGBAPY6rPKl2NT/K7XfRCGm1sbWjUQyDShscwuWJ5+kD0yudnT/ZEJ1
M3+KS/iOOAoHDdEDi9crRvMl0UfNa8MAcDKHflzxg2jg/QI+fTBjPP5GOX0lkZ9g
z6VePoVoQw2gpPFVNPPTxKfk27tEzbaffvOLGBEih0Kb7HTINkW8rIlzAoGBAM9y
1yr+jvfS1cGFtNU+Gotoihw2eMKtIqR03Yn3n0PK1nVCDKqwdUqCypz4+ml6cxRK
J8+Pfdh7D+ZJd4LEG6Y4QRDLuv5OA700tUoSHxMSNn3q9As4+T3MUyYxWKvTeu3U
f2NWP9ePU0lV8ttk7YlpVRaPQmc1qwooBA/z/8AdAoGAW9x0HWqmRICWTBnpjyxx
QGlW9rQ9mHEtUotIaRSJ6K/F3cxSGUEkX1a3FRnp6kPLcckC6NlqdNgNBd6rb2rA
cPl/uSkZP42Als+9YMoFPU/xrrDPbUhu72EDrj3Bllnyb168jKLa4VBOccUvggxr
Dm08I1hgYgdN5huzs7y6GeUCgYEAj+AZJSOJ6o1aXS6rfV3mMRve9bQ9yt8jcKXw
5HhOCEmMtaSKfnOF1Ziih34Sxsb7O2428DiX0mV/YHtBnPsAJidL0SdLWIapBzeg
KHArByIRkwE6IvJvwpGMdaex1PIGhx5i/3VZL9qiq/ElT05PhIb+UXgoWMabCp84
OgxDK20CgYAeaFo8BdQ7FmVX2+EEejF+8xSge6WVLtkaon8bqcn6P0O8lLypoOhd
mJAYH8WU+UAy9pecUnDZj14LAGNVmYcse8HFX71MoshnvCTFEPVo4rZxIAGwMpeJ
5jgQ3slYLpqrGlcbLgUXBUgzEO684Wk/UV9DFPlHALVqCfXQ9dpJPg==
-----END RSA PRIVATE KEY-----`
jsonKey = fmt.Sprintf(`{"private_key":"%[1]s", "client_email":"%[2]s", "type": "service_account"}`,
strings.Replace(dummyPrivateKey, "\n", "\\n", -1), email)
)
func TestJwtProvider(t *testing.T) {
token := "asdhflkjsdfkjhsdf"
// Modeled after oauth2/jwt_test.go
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(fmt.Sprintf(`{
"access_token": "%[1]s",
"scope": "user",
"token_type": "bearer",
"expires_in": 3600
}`, token)))
}))
defer ts.Close()
file, err := ioutil.TempFile(os.TempDir(), "temp")
if err != nil {
t.Fatalf("Error creating temp file: %v", err)
}
filename := file.Name()
defer os.Remove(filename)
_, err = file.WriteString(jsonKey)
if err != nil {
t.Fatalf("Error writing temp file: %v", err)
}
provider := &jwtProvider{
path: &filename,
tokenUrl: ts.URL,
}
if !provider.Enabled() {
t.Fatalf("Provider is unexpectedly disabled")
}
keyring := &credentialprovider.BasicDockerKeyring{}
keyring.Add(provider.Provide())
// Verify that we get the expected username/password combo for
// a gcr.io image name.
registryUrl := "gcr.io/foo/bar"
creds, ok := keyring.Lookup(registryUrl)
if !ok {
t.Errorf("Didn't find expected URL: %s", registryUrl)
return
}
if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if "_token" != val.Username {
t.Errorf("Unexpected username value, want: _token, got: %s", val.Username)
}
if token != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", token, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}

View File

@ -0,0 +1,298 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gcp_credentials
import (
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/golang/glog"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/kubernetes/pkg/credentialprovider"
)
const (
metadataUrl = "http://metadata.google.internal./computeMetadata/v1/"
metadataAttributes = metadataUrl + "instance/attributes/"
dockerConfigKey = metadataAttributes + "google-dockercfg"
dockerConfigUrlKey = metadataAttributes + "google-dockercfg-url"
serviceAccounts = metadataUrl + "instance/service-accounts/"
metadataScopes = metadataUrl + "instance/service-accounts/default/scopes"
metadataToken = metadataUrl + "instance/service-accounts/default/token"
metadataEmail = metadataUrl + "instance/service-accounts/default/email"
storageScopePrefix = "https://www.googleapis.com/auth/devstorage"
cloudPlatformScopePrefix = "https://www.googleapis.com/auth/cloud-platform"
googleProductName = "Google"
defaultServiceAccount = "default/"
)
// Product file path that contains the cloud service name.
// This is a variable instead of a const to enable testing.
var gceProductNameFile = "/sys/class/dmi/id/product_name"
// For these urls, the parts of the host name can be glob, for example '*.gcr.io" will match
// "foo.gcr.io" and "bar.gcr.io".
var containerRegistryUrls = []string{"container.cloud.google.com", "gcr.io", "*.gcr.io"}
var metadataHeader = &http.Header{
"Metadata-Flavor": []string{"Google"},
}
// A DockerConfigProvider that reads its configuration from Google
// Compute Engine metadata.
type metadataProvider struct {
Client *http.Client
}
// A DockerConfigProvider that reads its configuration from a specific
// Google Compute Engine metadata key: 'google-dockercfg'.
type dockerConfigKeyProvider struct {
metadataProvider
}
// A DockerConfigProvider that reads its configuration from a URL read from
// a specific Google Compute Engine metadata key: 'google-dockercfg-url'.
type dockerConfigUrlKeyProvider struct {
metadataProvider
}
// A DockerConfigProvider that provides a dockercfg with:
// Username: "_token"
// Password: "{access token from metadata}"
type containerRegistryProvider struct {
metadataProvider
}
// init registers the various means by which credentials may
// be resolved on GCP.
func init() {
tr := utilnet.SetTransportDefaults(&http.Transport{})
metadataHTTPClientTimeout := time.Second * 10
httpClient := &http.Client{
Transport: tr,
Timeout: metadataHTTPClientTimeout,
}
credentialprovider.RegisterCredentialProvider("google-dockercfg",
&credentialprovider.CachingDockerConfigProvider{
Provider: &dockerConfigKeyProvider{
metadataProvider{Client: httpClient},
},
Lifetime: 60 * time.Second,
})
credentialprovider.RegisterCredentialProvider("google-dockercfg-url",
&credentialprovider.CachingDockerConfigProvider{
Provider: &dockerConfigUrlKeyProvider{
metadataProvider{Client: httpClient},
},
Lifetime: 60 * time.Second,
})
credentialprovider.RegisterCredentialProvider("google-container-registry",
// Never cache this. The access token is already
// cached by the metadata service.
&containerRegistryProvider{
metadataProvider{Client: httpClient},
})
}
// Returns true if it finds a local GCE VM.
// Looks at a product file that is an undocumented API.
func onGCEVM() bool {
data, err := ioutil.ReadFile(gceProductNameFile)
if err != nil {
glog.V(2).Infof("Error while reading product_name: %v", err)
return false
}
return strings.Contains(string(data), googleProductName)
}
// Enabled implements DockerConfigProvider for all of the Google implementations.
func (g *metadataProvider) Enabled() bool {
return onGCEVM()
}
// LazyProvide implements DockerConfigProvider. Should never be called.
func (g *dockerConfigKeyProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
return nil
}
// Provide implements DockerConfigProvider
func (g *dockerConfigKeyProvider) Provide() credentialprovider.DockerConfig {
// Read the contents of the google-dockercfg metadata key and
// parse them as an alternate .dockercfg
if cfg, err := credentialprovider.ReadDockerConfigFileFromUrl(dockerConfigKey, g.Client, metadataHeader); err != nil {
glog.Errorf("while reading 'google-dockercfg' metadata: %v", err)
} else {
return cfg
}
return credentialprovider.DockerConfig{}
}
// LazyProvide implements DockerConfigProvider. Should never be called.
func (g *dockerConfigUrlKeyProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
return nil
}
// Provide implements DockerConfigProvider
func (g *dockerConfigUrlKeyProvider) Provide() credentialprovider.DockerConfig {
// Read the contents of the google-dockercfg-url key and load a .dockercfg from there
if url, err := credentialprovider.ReadUrl(dockerConfigUrlKey, g.Client, metadataHeader); err != nil {
glog.Errorf("while reading 'google-dockercfg-url' metadata: %v", err)
} else {
if strings.HasPrefix(string(url), "http") {
if cfg, err := credentialprovider.ReadDockerConfigFileFromUrl(string(url), g.Client, nil); err != nil {
glog.Errorf("while reading 'google-dockercfg-url'-specified url: %s, %v", string(url), err)
} else {
return cfg
}
} else {
// TODO(mattmoor): support reading alternate scheme URLs (e.g. gs:// or s3://)
glog.Errorf("Unsupported URL scheme: %s", string(url))
}
}
return credentialprovider.DockerConfig{}
}
// runcWithBackoff runs input function `f` with an exponential backoff.
// Note that this method can block indefinitely.
func runWithBackoff(f func() ([]byte, error)) []byte {
var backoff = 100 * time.Millisecond
const maxBackoff = time.Minute
for {
value, err := f()
if err == nil {
return value
}
time.Sleep(backoff)
backoff = backoff * 2
if backoff > maxBackoff {
backoff = maxBackoff
}
}
}
// Enabled implements a special metadata-based check, which verifies the
// storage scope is available on the GCE VM.
// If running on a GCE VM, check if 'default' service account exists.
// If it does not exist, assume that registry is not enabled.
// If default service account exists, check if relevant scopes exist in the default service account.
// The metadata service can become temporarily inaccesible. Hence all requests to the metadata
// service will be retried until the metadata server returns a `200`.
// It is expected that "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/" will return a `200`
// and "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/scopes" will also return `200`.
// More information on metadata service can be found here - https://cloud.google.com/compute/docs/storing-retrieving-metadata
func (g *containerRegistryProvider) Enabled() bool {
if !onGCEVM() {
return false
}
// Given that we are on GCE, we should keep retrying until the metadata server responds.
value := runWithBackoff(func() ([]byte, error) {
value, err := credentialprovider.ReadUrl(serviceAccounts, g.Client, metadataHeader)
if err != nil {
glog.V(2).Infof("Failed to Get service accounts from gce metadata server: %v", err)
}
return value, err
})
// We expect the service account to return a list of account directories separated by newlines, e.g.,
// sv-account-name1/
// sv-account-name2/
// ref: https://cloud.google.com/compute/docs/storing-retrieving-metadata
defaultServiceAccountExists := false
for _, sa := range strings.Split(string(value), "\n") {
if strings.TrimSpace(sa) == defaultServiceAccount {
defaultServiceAccountExists = true
break
}
}
if !defaultServiceAccountExists {
glog.V(2).Infof("'default' service account does not exist. Found following service accounts: %q", string(value))
return false
}
url := metadataScopes + "?alt=json"
value = runWithBackoff(func() ([]byte, error) {
value, err := credentialprovider.ReadUrl(url, g.Client, metadataHeader)
if err != nil {
glog.V(2).Infof("Failed to Get scopes in default service account from gce metadata server: %v", err)
}
return value, err
})
var scopes []string
if err := json.Unmarshal(value, &scopes); err != nil {
glog.Errorf("Failed to unmarshal scopes: %v", err)
return false
}
for _, v := range scopes {
// cloudPlatformScope implies storage scope.
if strings.HasPrefix(v, storageScopePrefix) || strings.HasPrefix(v, cloudPlatformScopePrefix) {
return true
}
}
glog.Warningf("Google container registry is disabled, no storage scope is available: %s", value)
return false
}
// tokenBlob is used to decode the JSON blob containing an access token
// that is returned by GCE metadata.
type tokenBlob struct {
AccessToken string `json:"access_token"`
}
// LazyProvide implements DockerConfigProvider. Should never be called.
func (g *containerRegistryProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
return nil
}
// Provide implements DockerConfigProvider
func (g *containerRegistryProvider) Provide() credentialprovider.DockerConfig {
cfg := credentialprovider.DockerConfig{}
tokenJsonBlob, err := credentialprovider.ReadUrl(metadataToken, g.Client, metadataHeader)
if err != nil {
glog.Errorf("while reading access token endpoint: %v", err)
return cfg
}
email, err := credentialprovider.ReadUrl(metadataEmail, g.Client, metadataHeader)
if err != nil {
glog.Errorf("while reading email endpoint: %v", err)
return cfg
}
var parsedBlob tokenBlob
if err := json.Unmarshal([]byte(tokenJsonBlob), &parsedBlob); err != nil {
glog.Errorf("while parsing json blob %s: %v", tokenJsonBlob, err)
return cfg
}
entry := credentialprovider.DockerConfigEntry{
Username: "_token",
Password: parsedBlob.AccessToken,
Email: string(email),
}
// Add our entry for each of the supported container registry URLs
for _, k := range containerRegistryUrls {
cfg[k] = entry
}
return cfg
}

View File

@ -0,0 +1,441 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gcp_credentials
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
"strings"
"testing"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/kubernetes/pkg/credentialprovider"
)
func createProductNameFile() (string, error) {
file, err := ioutil.TempFile("", "")
if err != nil {
return "", fmt.Errorf("failed to create temporary test file: %v", err)
}
return file.Name(), ioutil.WriteFile(file.Name(), []byte("Google"), 0600)
}
func TestDockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
registryUrl := "hello.kubernetes.io"
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, registryUrl, email, auth)
var err error
gceProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
const probeEndpoint = "/computeMetadata/v1/"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the one metadata key.
if probeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
} else if strings.HasSuffix(dockerConfigKey, r.URL.Path) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, sampleDockerConfig)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &dockerConfigKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
t.Errorf("Provider is unexpectedly disabled")
}
keyring.Add(provider.Provide())
creds, ok := keyring.Lookup(registryUrl)
if !ok {
t.Errorf("Didn't find expected URL: %s", registryUrl)
return
}
if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
registryUrl := "hello.kubernetes.io"
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, registryUrl, email, auth)
var err error
gceProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
const probeEndpoint = "/computeMetadata/v1/"
const valueEndpoint = "/my/value"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the URL key and the value endpoint
if probeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
} else if valueEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, sampleDockerConfig)
} else if strings.HasSuffix(dockerConfigUrlKey, r.URL.Path) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/text")
fmt.Fprint(w, "http://foo.bar.com"+valueEndpoint)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &dockerConfigUrlKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
t.Errorf("Provider is unexpectedly disabled")
}
keyring.Add(provider.Provide())
creds, ok := keyring.Lookup(registryUrl)
if !ok {
t.Errorf("Didn't find expected URL: %s", registryUrl)
return
}
if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestContainerRegistryBasics(t *testing.T) {
registryUrl := "container.cloud.google.com"
email := "1234@project.gserviceaccount.com"
token := &tokenBlob{AccessToken: "ya26.lots-of-indiscernible-garbage"}
const (
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
scopeEndpoint = defaultEndpoint + "scopes"
emailEndpoint = defaultEndpoint + "email"
tokenEndpoint = defaultEndpoint + "token"
)
var err error
gceProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the URL key and the value endpoint
if scopeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `["%s.read_write"]`, storageScopePrefix)
} else if emailEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, email)
} else if tokenEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
bytes, err := json.Marshal(token)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
fmt.Fprintln(w, string(bytes))
} else if serviceAccountsEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "default/\ncustom")
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
t.Errorf("Provider is unexpectedly disabled")
}
keyring.Add(provider.Provide())
creds, ok := keyring.Lookup(registryUrl)
if !ok {
t.Errorf("Didn't find expected URL: %s", registryUrl)
return
}
if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if "_token" != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", "_token", val.Username)
}
if token.AccessToken != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", token.AccessToken, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestContainerRegistryNoServiceAccount(t *testing.T) {
const (
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the URL key and the value endpoint
if serviceAccountsEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
bytes, err := json.Marshal([]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
fmt.Fprintln(w, string(bytes))
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
var err error
gceProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
})
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if provider.Enabled() {
t.Errorf("Provider is unexpectedly enabled")
}
}
func TestContainerRegistryNoStorageScope(t *testing.T) {
const (
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
scopeEndpoint = defaultEndpoint + "scopes"
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the URL key and the value endpoint
if scopeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `["https://www.googleapis.com/auth/compute.read_write"]`)
} else if serviceAccountsEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "default/\ncustom")
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
var err error
gceProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
})
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if provider.Enabled() {
t.Errorf("Provider is unexpectedly enabled")
}
}
func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
const (
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
scopeEndpoint = defaultEndpoint + "scopes"
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the URL key and the value endpoint
if scopeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `["https://www.googleapis.com/auth/compute.read_write","https://www.googleapis.com/auth/cloud-platform.read-only"]`)
} else if serviceAccountsEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, "default/\ncustom")
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
var err error
gceProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
})
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
t.Errorf("Provider is unexpectedly disabled")
}
}
func TestAllProvidersNoMetadata(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
})
providers := []credentialprovider.DockerConfigProvider{
&dockerConfigKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
},
&dockerConfigUrlKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
},
&containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
},
}
for _, provider := range providers {
if provider.Enabled() {
t.Errorf("Provider %s is unexpectedly enabled", reflect.TypeOf(provider).String())
}
}
}

View File

@ -0,0 +1,338 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
import (
"encoding/json"
"net"
"net/url"
"path/filepath"
"sort"
"strings"
"github.com/golang/glog"
dockertypes "github.com/docker/docker/api/types"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets"
)
// DockerKeyring tracks a set of docker registry credentials, maintaining a
// reverse index across the registry endpoints. A registry endpoint is made
// up of a host (e.g. registry.example.com), but it may also contain a path
// (e.g. registry.example.com/foo) This index is important for two reasons:
// - registry endpoints may overlap, and when this happens we must find the
// most specific match for a given image
// - iterating a map does not yield predictable results
type DockerKeyring interface {
Lookup(image string) ([]LazyAuthConfiguration, bool)
}
// BasicDockerKeyring is a trivial map-backed implementation of DockerKeyring
type BasicDockerKeyring struct {
index []string
creds map[string][]LazyAuthConfiguration
}
// lazyDockerKeyring is an implementation of DockerKeyring that lazily
// materializes its dockercfg based on a set of dockerConfigProviders.
type lazyDockerKeyring struct {
Providers []DockerConfigProvider
}
// LazyAuthConfiguration wraps dockertypes.AuthConfig, potentially deferring its
// binding. If Provider is non-nil, it will be used to obtain new credentials
// by calling LazyProvide() on it.
type LazyAuthConfiguration struct {
dockertypes.AuthConfig
Provider DockerConfigProvider
}
func DockerConfigEntryToLazyAuthConfiguration(ident DockerConfigEntry) LazyAuthConfiguration {
return LazyAuthConfiguration{
AuthConfig: dockertypes.AuthConfig{
Username: ident.Username,
Password: ident.Password,
Email: ident.Email,
},
}
}
func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
if dk.index == nil {
dk.index = make([]string, 0)
dk.creds = make(map[string][]LazyAuthConfiguration)
}
for loc, ident := range cfg {
var creds LazyAuthConfiguration
if ident.Provider != nil {
creds = LazyAuthConfiguration{
Provider: ident.Provider,
}
} else {
creds = DockerConfigEntryToLazyAuthConfiguration(ident)
}
value := loc
if !strings.HasPrefix(value, "https://") && !strings.HasPrefix(value, "http://") {
value = "https://" + value
}
parsed, err := url.Parse(value)
if err != nil {
glog.Errorf("Entry %q in dockercfg invalid (%v), ignoring", loc, err)
continue
}
// The docker client allows exact matches:
// foo.bar.com/namespace
// Or hostname matches:
// foo.bar.com
// It also considers /v2/ and /v1/ equivalent to the hostname
// See ResolveAuthConfig in docker/registry/auth.go.
effectivePath := parsed.Path
if strings.HasPrefix(effectivePath, "/v2/") || strings.HasPrefix(effectivePath, "/v1/") {
effectivePath = effectivePath[3:]
}
var key string
if (len(effectivePath) > 0) && (effectivePath != "/") {
key = parsed.Host + effectivePath
} else {
key = parsed.Host
}
dk.creds[key] = append(dk.creds[key], creds)
dk.index = append(dk.index, key)
}
eliminateDupes := sets.NewString(dk.index...)
dk.index = eliminateDupes.List()
// Update the index used to identify which credentials to use for a given
// image. The index is reverse-sorted so more specific paths are matched
// first. For example, if for the given image "quay.io/coreos/etcd",
// credentials for "quay.io/coreos" should match before "quay.io".
sort.Sort(sort.Reverse(sort.StringSlice(dk.index)))
}
const (
defaultRegistryHost = "index.docker.io"
defaultRegistryKey = defaultRegistryHost + "/v1/"
)
// isDefaultRegistryMatch determines whether the given image will
// pull from the default registry (DockerHub) based on the
// characteristics of its name.
func isDefaultRegistryMatch(image string) bool {
parts := strings.SplitN(image, "/", 2)
if len(parts[0]) == 0 {
return false
}
if len(parts) == 1 {
// e.g. library/ubuntu
return true
}
if parts[0] == "docker.io" || parts[0] == "index.docker.io" {
// resolve docker.io/image and index.docker.io/image as default registry
return true
}
// From: http://blog.docker.com/2013/07/how-to-use-your-own-registry/
// Docker looks for either a “.” (domain separator) or “:” (port separator)
// to learn that the first part of the repository name is a location and not
// a user name.
return !strings.ContainsAny(parts[0], ".:")
}
// url.Parse require a scheme, but ours don't have schemes. Adding a
// scheme to make url.Parse happy, then clear out the resulting scheme.
func parseSchemelessUrl(schemelessUrl string) (*url.URL, error) {
parsed, err := url.Parse("https://" + schemelessUrl)
if err != nil {
return nil, err
}
// clear out the resulting scheme
parsed.Scheme = ""
return parsed, nil
}
// split the host name into parts, as well as the port
func splitUrl(url *url.URL) (parts []string, port string) {
host, port, err := net.SplitHostPort(url.Host)
if err != nil {
// could not parse port
host, port = url.Host, ""
}
return strings.Split(host, "."), port
}
// overloaded version of urlsMatch, operating on strings instead of URLs.
func urlsMatchStr(glob string, target string) (bool, error) {
globUrl, err := parseSchemelessUrl(glob)
if err != nil {
return false, err
}
targetUrl, err := parseSchemelessUrl(target)
if err != nil {
return false, err
}
return urlsMatch(globUrl, targetUrl)
}
// check whether the given target url matches the glob url, which may have
// glob wild cards in the host name.
//
// Examples:
// globUrl=*.docker.io, targetUrl=blah.docker.io => match
// globUrl=*.docker.io, targetUrl=not.right.io => no match
//
// Note that we don't support wildcards in ports and paths yet.
func urlsMatch(globUrl *url.URL, targetUrl *url.URL) (bool, error) {
globUrlParts, globPort := splitUrl(globUrl)
targetUrlParts, targetPort := splitUrl(targetUrl)
if globPort != targetPort {
// port doesn't match
return false, nil
}
if len(globUrlParts) != len(targetUrlParts) {
// host name does not have the same number of parts
return false, nil
}
if !strings.HasPrefix(targetUrl.Path, globUrl.Path) {
// the path of the credential must be a prefix
return false, nil
}
for k, globUrlPart := range globUrlParts {
targetUrlPart := targetUrlParts[k]
matched, err := filepath.Match(globUrlPart, targetUrlPart)
if err != nil {
return false, err
}
if !matched {
// glob mismatch for some part
return false, nil
}
}
// everything matches
return true, nil
}
// Lookup implements the DockerKeyring method for fetching credentials based on image name.
// Multiple credentials may be returned if there are multiple potentially valid credentials
// available. This allows for rotation.
func (dk *BasicDockerKeyring) Lookup(image string) ([]LazyAuthConfiguration, bool) {
// range over the index as iterating over a map does not provide a predictable ordering
ret := []LazyAuthConfiguration{}
for _, k := range dk.index {
// both k and image are schemeless URLs because even though schemes are allowed
// in the credential configurations, we remove them in Add.
if matched, _ := urlsMatchStr(k, image); !matched {
continue
}
ret = append(ret, dk.creds[k]...)
}
if len(ret) > 0 {
return ret, true
}
// Use credentials for the default registry if provided, and appropriate
if isDefaultRegistryMatch(image) {
if auth, ok := dk.creds[defaultRegistryHost]; ok {
return auth, true
}
}
return []LazyAuthConfiguration{}, false
}
// Lookup implements the DockerKeyring method for fetching credentials
// based on image name.
func (dk *lazyDockerKeyring) Lookup(image string) ([]LazyAuthConfiguration, bool) {
keyring := &BasicDockerKeyring{}
for _, p := range dk.Providers {
keyring.Add(p.Provide())
}
return keyring.Lookup(image)
}
type FakeKeyring struct {
auth []LazyAuthConfiguration
ok bool
}
func (f *FakeKeyring) Lookup(image string) ([]LazyAuthConfiguration, bool) {
return f.auth, f.ok
}
// unionDockerKeyring delegates to a set of keyrings.
type unionDockerKeyring struct {
keyrings []DockerKeyring
}
func (k *unionDockerKeyring) Lookup(image string) ([]LazyAuthConfiguration, bool) {
authConfigs := []LazyAuthConfiguration{}
for _, subKeyring := range k.keyrings {
if subKeyring == nil {
continue
}
currAuthResults, _ := subKeyring.Lookup(image)
authConfigs = append(authConfigs, currAuthResults...)
}
return authConfigs, (len(authConfigs) > 0)
}
// MakeDockerKeyring inspects the passedSecrets to see if they contain any DockerConfig secrets. If they do,
// then a DockerKeyring is built based on every hit and unioned with the defaultKeyring.
// If they do not, then the default keyring is returned
func MakeDockerKeyring(passedSecrets []v1.Secret, defaultKeyring DockerKeyring) (DockerKeyring, error) {
passedCredentials := []DockerConfig{}
for _, passedSecret := range passedSecrets {
if dockerConfigJsonBytes, dockerConfigJsonExists := passedSecret.Data[v1.DockerConfigJsonKey]; (passedSecret.Type == v1.SecretTypeDockerConfigJson) && dockerConfigJsonExists && (len(dockerConfigJsonBytes) > 0) {
dockerConfigJson := DockerConfigJson{}
if err := json.Unmarshal(dockerConfigJsonBytes, &dockerConfigJson); err != nil {
return nil, err
}
passedCredentials = append(passedCredentials, dockerConfigJson.Auths)
} else if dockercfgBytes, dockercfgExists := passedSecret.Data[v1.DockerConfigKey]; (passedSecret.Type == v1.SecretTypeDockercfg) && dockercfgExists && (len(dockercfgBytes) > 0) {
dockercfg := DockerConfig{}
if err := json.Unmarshal(dockercfgBytes, &dockercfg); err != nil {
return nil, err
}
passedCredentials = append(passedCredentials, dockercfg)
}
}
if len(passedCredentials) > 0 {
basicKeyring := &BasicDockerKeyring{}
for _, currCredentials := range passedCredentials {
basicKeyring.Add(currCredentials)
}
return &unionDockerKeyring{[]DockerKeyring{basicKeyring, defaultKeyring}}, nil
}
return defaultKeyring, nil
}

View File

@ -0,0 +1,618 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
import (
"encoding/base64"
"fmt"
"reflect"
"testing"
dockertypes "github.com/docker/docker/api/types"
)
func TestUrlsMatch(t *testing.T) {
tests := []struct {
globUrl string
targetUrl string
matchExpected bool
}{
// match when there is no path component
{
globUrl: "*.kubernetes.io",
targetUrl: "prefix.kubernetes.io",
matchExpected: true,
},
{
globUrl: "prefix.*.io",
targetUrl: "prefix.kubernetes.io",
matchExpected: true,
},
{
globUrl: "prefix.kubernetes.*",
targetUrl: "prefix.kubernetes.io",
matchExpected: true,
},
{
globUrl: "*-good.kubernetes.io",
targetUrl: "prefix-good.kubernetes.io",
matchExpected: true,
},
// match with path components
{
globUrl: "*.kubernetes.io/blah",
targetUrl: "prefix.kubernetes.io/blah",
matchExpected: true,
},
{
globUrl: "prefix.*.io/foo",
targetUrl: "prefix.kubernetes.io/foo/bar",
matchExpected: true,
},
// match with path components and ports
{
globUrl: "*.kubernetes.io:1111/blah",
targetUrl: "prefix.kubernetes.io:1111/blah",
matchExpected: true,
},
{
globUrl: "prefix.*.io:1111/foo",
targetUrl: "prefix.kubernetes.io:1111/foo/bar",
matchExpected: true,
},
// no match when number of parts mismatch
{
globUrl: "*.kubernetes.io",
targetUrl: "kubernetes.io",
matchExpected: false,
},
{
globUrl: "*.*.kubernetes.io",
targetUrl: "prefix.kubernetes.io",
matchExpected: false,
},
{
globUrl: "*.*.kubernetes.io",
targetUrl: "kubernetes.io",
matchExpected: false,
},
// no match when some parts mismatch
{
globUrl: "kubernetes.io",
targetUrl: "kubernetes.com",
matchExpected: false,
},
{
globUrl: "k*.io",
targetUrl: "quay.io",
matchExpected: false,
},
// no match when ports mismatch
{
globUrl: "*.kubernetes.io:1234/blah",
targetUrl: "prefix.kubernetes.io:1111/blah",
matchExpected: false,
},
{
globUrl: "prefix.*.io/foo",
targetUrl: "prefix.kubernetes.io:1111/foo/bar",
matchExpected: false,
},
}
for _, test := range tests {
matched, _ := urlsMatchStr(test.globUrl, test.targetUrl)
if matched != test.matchExpected {
t.Errorf("Expected match result of %s and %s to be %t, but was %t",
test.globUrl, test.targetUrl, test.matchExpected, matched)
}
}
}
func TestDockerKeyringForGlob(t *testing.T) {
tests := []struct {
globUrl string
targetUrl string
}{
{
globUrl: "https://hello.kubernetes.io",
targetUrl: "hello.kubernetes.io",
},
{
globUrl: "https://*.docker.io",
targetUrl: "prefix.docker.io",
},
{
globUrl: "https://prefix.*.io",
targetUrl: "prefix.docker.io",
},
{
globUrl: "https://prefix.docker.*",
targetUrl: "prefix.docker.io",
},
{
globUrl: "https://*.docker.io/path",
targetUrl: "prefix.docker.io/path",
},
{
globUrl: "https://prefix.*.io/path",
targetUrl: "prefix.docker.io/path/subpath",
},
{
globUrl: "https://prefix.docker.*/path",
targetUrl: "prefix.docker.io/path",
},
{
globUrl: "https://*.docker.io:8888",
targetUrl: "prefix.docker.io:8888",
},
{
globUrl: "https://prefix.*.io:8888",
targetUrl: "prefix.docker.io:8888",
},
{
globUrl: "https://prefix.docker.*:8888",
targetUrl: "prefix.docker.io:8888",
},
{
globUrl: "https://*.docker.io/path:1111",
targetUrl: "prefix.docker.io/path:1111",
},
{
globUrl: "https://*.docker.io/v1/",
targetUrl: "prefix.docker.io/path:1111",
},
{
globUrl: "https://*.docker.io/v2/",
targetUrl: "prefix.docker.io/path:1111",
},
{
globUrl: "https://prefix.docker.*/path:1111",
targetUrl: "prefix.docker.io/path:1111",
},
{
globUrl: "prefix.docker.io:1111",
targetUrl: "prefix.docker.io:1111/path",
},
{
globUrl: "*.docker.io:1111",
targetUrl: "prefix.docker.io:1111/path",
},
}
for i, test := range tests {
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"%s": {
"email": %q,
"auth": %q
}
}`, test.globUrl, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
creds, ok := keyring.Lookup(test.targetUrl + "/foo/bar")
if !ok {
t.Errorf("%d: Didn't find expected URL: %s", i, test.targetUrl)
continue
}
val := creds[0]
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
}
func TestKeyringMiss(t *testing.T) {
tests := []struct {
globUrl string
lookupUrl string
}{
{
globUrl: "https://hello.kubernetes.io",
lookupUrl: "world.mesos.org/foo/bar",
},
{
globUrl: "https://*.docker.com",
lookupUrl: "prefix.docker.io",
},
{
globUrl: "https://suffix.*.io",
lookupUrl: "prefix.docker.io",
},
{
globUrl: "https://prefix.docker.c*",
lookupUrl: "prefix.docker.io",
},
{
globUrl: "https://prefix.*.io/path:1111",
lookupUrl: "prefix.docker.io/path/subpath:1111",
},
{
globUrl: "suffix.*.io",
lookupUrl: "prefix.docker.io",
},
}
for _, test := range tests {
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"%s": {
"email": %q,
"auth": %q
}
}`, test.globUrl, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
_, ok := keyring.Lookup(test.lookupUrl + "/foo/bar")
if ok {
t.Errorf("Expected not to find URL %s, but found", test.lookupUrl)
}
}
}
func TestKeyringMissWithDockerHubCredentials(t *testing.T) {
url := defaultRegistryKey
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
val, ok := keyring.Lookup("world.mesos.org/foo/bar")
if ok {
t.Errorf("Found unexpected credential: %+v", val)
}
}
func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
url := defaultRegistryKey
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
creds, ok := keyring.Lookup("google/docker-registry")
if !ok {
t.Errorf("Didn't find expected URL: %s", url)
return
}
if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
url := defaultRegistryKey
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
creds, ok := keyring.Lookup("jenkins")
if !ok {
t.Errorf("Didn't find expected URL: %s", url)
return
}
if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
url := defaultRegistryKey
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
creds, ok := keyring.Lookup(url + "/google/docker-registry")
if !ok {
t.Errorf("Didn't find expected URL: %s", url)
return
}
if len(creds) > 2 {
t.Errorf("Got more hits than expected: %s", creds)
}
val := creds[0]
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestIsDefaultRegistryMatch(t *testing.T) {
samples := []map[bool]string{
{true: "foo/bar"},
{true: "docker.io/foo/bar"},
{true: "index.docker.io/foo/bar"},
{true: "foo"},
{false: ""},
{false: "registry.tld/foo/bar"},
{false: "registry:5000/foo/bar"},
{false: "myhostdocker.io/foo/bar"},
}
for _, sample := range samples {
for expected, imageName := range sample {
if got := isDefaultRegistryMatch(imageName); got != expected {
t.Errorf("Expected '%s' to be %t, got %t", imageName, expected, got)
}
}
}
}
type testProvider struct {
Count int
}
// Enabled implements dockerConfigProvider
func (d *testProvider) Enabled() bool {
return true
}
// LazyProvide implements dockerConfigProvider. Should never be called.
func (d *testProvider) LazyProvide() *DockerConfigEntry {
return nil
}
// Provide implements dockerConfigProvider
func (d *testProvider) Provide() DockerConfig {
d.Count += 1
return DockerConfig{}
}
func TestLazyKeyring(t *testing.T) {
provider := &testProvider{
Count: 0,
}
lazy := &lazyDockerKeyring{
Providers: []DockerConfigProvider{
provider,
},
}
if provider.Count != 0 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
lazy.Lookup("foo")
if provider.Count != 1 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
lazy.Lookup("foo")
if provider.Count != 2 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
lazy.Lookup("foo")
if provider.Count != 3 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
}
func TestDockerKeyringLookup(t *testing.T) {
ada := LazyAuthConfiguration{
AuthConfig: dockertypes.AuthConfig{
Username: "ada",
Password: "smash",
Email: "ada@example.com",
},
}
grace := LazyAuthConfiguration{
AuthConfig: dockertypes.AuthConfig{
Username: "grace",
Password: "squash",
Email: "grace@example.com",
},
}
dk := &BasicDockerKeyring{}
dk.Add(DockerConfig{
"bar.example.com/pong": DockerConfigEntry{
Username: grace.Username,
Password: grace.Password,
Email: grace.Email,
},
"bar.example.com": DockerConfigEntry{
Username: ada.Username,
Password: ada.Password,
Email: ada.Email,
},
})
tests := []struct {
image string
match []LazyAuthConfiguration
ok bool
}{
// direct match
{"bar.example.com", []LazyAuthConfiguration{ada}, true},
// direct match deeper than other possible matches
{"bar.example.com/pong", []LazyAuthConfiguration{grace, ada}, true},
// no direct match, deeper path ignored
{"bar.example.com/ping", []LazyAuthConfiguration{ada}, true},
// match first part of path token
{"bar.example.com/pongz", []LazyAuthConfiguration{grace, ada}, true},
// match regardless of sub-path
{"bar.example.com/pong/pang", []LazyAuthConfiguration{grace, ada}, true},
// no host match
{"example.com", []LazyAuthConfiguration{}, false},
{"foo.example.com", []LazyAuthConfiguration{}, false},
}
for i, tt := range tests {
match, ok := dk.Lookup(tt.image)
if tt.ok != ok {
t.Errorf("case %d: expected ok=%t, got %t", i, tt.ok, ok)
}
if !reflect.DeepEqual(tt.match, match) {
t.Errorf("case %d: expected match=%#v, got %#v", i, tt.match, match)
}
}
}
// This validates that dockercfg entries with a scheme and url path are properly matched
// by images that only match the hostname.
// NOTE: the above covers the case of a more specific match trumping just hostname.
func TestIssue3797(t *testing.T) {
rex := LazyAuthConfiguration{
AuthConfig: dockertypes.AuthConfig{
Username: "rex",
Password: "tiny arms",
Email: "rex@example.com",
},
}
dk := &BasicDockerKeyring{}
dk.Add(DockerConfig{
"https://quay.io/v1/": DockerConfigEntry{
Username: rex.Username,
Password: rex.Password,
Email: rex.Email,
},
})
tests := []struct {
image string
match []LazyAuthConfiguration
ok bool
}{
// direct match
{"quay.io", []LazyAuthConfiguration{rex}, true},
// partial matches
{"quay.io/foo", []LazyAuthConfiguration{rex}, true},
{"quay.io/foo/bar", []LazyAuthConfiguration{rex}, true},
}
for i, tt := range tests {
match, ok := dk.Lookup(tt.image)
if tt.ok != ok {
t.Errorf("case %d: expected ok=%t, got %t", i, tt.ok, ok)
}
if !reflect.DeepEqual(tt.match, match) {
t.Errorf("case %d: expected match=%#v, got %#v", i, tt.match, match)
}
}
}

View File

@ -0,0 +1,62 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
import (
"sync"
"github.com/golang/glog"
)
// All registered credential providers.
var providersMutex sync.Mutex
var providers = make(map[string]DockerConfigProvider)
// RegisterCredentialProvider is called by provider implementations on
// initialization to register themselves, like so:
// func init() {
// RegisterCredentialProvider("name", &myProvider{...})
// }
func RegisterCredentialProvider(name string, provider DockerConfigProvider) {
providersMutex.Lock()
defer providersMutex.Unlock()
_, found := providers[name]
if found {
glog.Fatalf("Credential provider %q was registered twice", name)
}
glog.V(4).Infof("Registered credential provider %q", name)
providers[name] = provider
}
// NewDockerKeyring creates a DockerKeyring to use for resolving credentials,
// which lazily draws from the set of registered credential providers.
func NewDockerKeyring() DockerKeyring {
keyring := &lazyDockerKeyring{
Providers: make([]DockerConfigProvider, 0),
}
// TODO(mattmoor): iterating over the map is non-deterministic. We should
// introduce the notion of priorities for conflict resolution.
for name, provider := range providers {
if provider.Enabled() {
glog.V(4).Infof("Registering credential provider: %v", name)
keyring.Providers = append(keyring.Providers, provider)
}
}
return keyring
}

View File

@ -0,0 +1,123 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
import (
"os"
"reflect"
"sync"
"time"
dockertypes "github.com/docker/docker/api/types"
"github.com/golang/glog"
)
// DockerConfigProvider is the interface that registered extensions implement
// to materialize 'dockercfg' credentials.
type DockerConfigProvider interface {
// Enabled returns true if the config provider is enabled.
// Implementations can be blocking - e.g. metadata server unavailable.
Enabled() bool
// Provide returns docker configuration.
// Implementations can be blocking - e.g. metadata server unavailable.
Provide() DockerConfig
// LazyProvide() gets called after URL matches have been performed, so the
// location used as the key in DockerConfig would be redundant.
LazyProvide() *DockerConfigEntry
}
func LazyProvide(creds LazyAuthConfiguration) dockertypes.AuthConfig {
if creds.Provider != nil {
entry := *creds.Provider.LazyProvide()
return DockerConfigEntryToLazyAuthConfiguration(entry).AuthConfig
} else {
return creds.AuthConfig
}
}
// A DockerConfigProvider that simply reads the .dockercfg file
type defaultDockerConfigProvider struct{}
// init registers our default provider, which simply reads the .dockercfg file.
func init() {
RegisterCredentialProvider(".dockercfg",
&CachingDockerConfigProvider{
Provider: &defaultDockerConfigProvider{},
Lifetime: 5 * time.Minute,
})
}
// CachingDockerConfigProvider implements DockerConfigProvider by composing
// with another DockerConfigProvider and caching the DockerConfig it provides
// for a pre-specified lifetime.
type CachingDockerConfigProvider struct {
Provider DockerConfigProvider
Lifetime time.Duration
// cache fields
cacheDockerConfig DockerConfig
expiration time.Time
mu sync.Mutex
}
// Enabled implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Enabled() bool {
return true
}
// Provide implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Provide() DockerConfig {
// Read the standard Docker credentials from .dockercfg
if cfg, err := ReadDockerConfigFile(); err == nil {
return cfg
} else if !os.IsNotExist(err) {
glog.V(4).Infof("Unable to parse Docker config file: %v", err)
}
return DockerConfig{}
}
// LazyProvide implements dockerConfigProvider. Should never be called.
func (d *defaultDockerConfigProvider) LazyProvide() *DockerConfigEntry {
return nil
}
// Enabled implements dockerConfigProvider
func (d *CachingDockerConfigProvider) Enabled() bool {
return d.Provider.Enabled()
}
// LazyProvide implements dockerConfigProvider. Should never be called.
func (d *CachingDockerConfigProvider) LazyProvide() *DockerConfigEntry {
return nil
}
// Provide implements dockerConfigProvider
func (d *CachingDockerConfigProvider) Provide() DockerConfig {
d.mu.Lock()
defer d.mu.Unlock()
// If the cache hasn't expired, return our cache
if time.Now().Before(d.expiration) {
return d.cacheDockerConfig
}
glog.V(2).Infof("Refreshing cache for provider: %v", reflect.TypeOf(d.Provider).String())
d.cacheDockerConfig = d.Provider.Provide()
d.expiration = time.Now().Add(d.Lifetime)
return d.cacheDockerConfig
}

View File

@ -0,0 +1,62 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
import (
"testing"
"time"
)
func TestCachingProvider(t *testing.T) {
provider := &testProvider{
Count: 0,
}
cache := &CachingDockerConfigProvider{
Provider: provider,
Lifetime: 1 * time.Second,
}
if provider.Count != 0 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
cache.Provide()
cache.Provide()
cache.Provide()
cache.Provide()
if provider.Count != 1 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
time.Sleep(cache.Lifetime)
cache.Provide()
cache.Provide()
cache.Provide()
cache.Provide()
if provider.Count != 2 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
time.Sleep(cache.Lifetime)
cache.Provide()
cache.Provide()
cache.Provide()
cache.Provide()
if provider.Count != 3 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
}

View File

@ -0,0 +1,45 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["rancher_registry_credentials_test.go"],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/rancher",
library = ":go_default_library",
deps = [
"//pkg/credentialprovider:go_default_library",
"//vendor/github.com/rancher/go-rancher/client:go_default_library",
],
)
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"rancher_registry_credentials.go",
],
importpath = "k8s.io/kubernetes/pkg/credentialprovider/rancher",
deps = [
"//pkg/credentialprovider:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/rancher/go-rancher/client:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -0,0 +1,17 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rancher_credentials

View File

@ -0,0 +1,145 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rancher_credentials
import (
"os"
"time"
"github.com/golang/glog"
"github.com/rancher/go-rancher/client"
"k8s.io/kubernetes/pkg/credentialprovider"
)
// rancher provider
type rancherProvider struct {
credGetter credentialsGetter
}
// credentials getter from Rancher private registry
type rancherCredentialsGetter struct {
client *client.RancherClient
}
type rConfig struct {
Global configGlobal
}
// An interface for testing purposes.
type credentialsGetter interface {
getCredentials() []registryCredential
}
type configGlobal struct {
CattleURL string `gcfg:"cattle-url"`
CattleAccessKey string `gcfg:"cattle-access-key"`
CattleSecretKey string `gcfg:"cattle-secret-key"`
}
type registryCredential struct {
credential *client.RegistryCredential
serverIP string
}
var rancherGetter = &rancherCredentialsGetter{}
func init() {
credentialprovider.RegisterCredentialProvider("rancher-registry-creds",
&credentialprovider.CachingDockerConfigProvider{
Provider: &rancherProvider{rancherGetter},
Lifetime: 30 * time.Second,
})
}
func (p *rancherProvider) Enabled() bool {
client, err := getRancherClient()
if err != nil {
return false
}
if client == nil {
return false
}
rancherGetter.client = client
return true
}
// LazyProvide implements DockerConfigProvider. Should never be called.
func (p *rancherProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
return nil
}
// Provide implements DockerConfigProvider.Provide, refreshing Rancher tokens on demand
func (p *rancherProvider) Provide() credentialprovider.DockerConfig {
cfg := credentialprovider.DockerConfig{}
for _, cred := range p.credGetter.getCredentials() {
entry := credentialprovider.DockerConfigEntry{
Username: cred.credential.PublicValue,
Password: cred.credential.SecretValue,
Email: cred.credential.Email,
}
cfg[cred.serverIP] = entry
}
return cfg
}
func (g *rancherCredentialsGetter) getCredentials() []registryCredential {
var registryCreds []registryCredential
credColl, err := g.client.RegistryCredential.List(client.NewListOpts())
if err != nil {
glog.Errorf("Failed to pull registry credentials from rancher %v", err)
return registryCreds
}
for _, cred := range credColl.Data {
registry := &client.Registry{}
if err = g.client.GetLink(cred.Resource, "registry", registry); err != nil {
glog.Errorf("Failed to pull registry from rancher %v", err)
return registryCreds
}
registryCred := registryCredential{
credential: &cred,
serverIP: registry.ServerAddress,
}
registryCreds = append(registryCreds, registryCred)
}
return registryCreds
}
func getRancherClient() (*client.RancherClient, error) {
url := os.Getenv("CATTLE_URL")
accessKey := os.Getenv("CATTLE_ACCESS_KEY")
secretKey := os.Getenv("CATTLE_SECRET_KEY")
if url == "" || accessKey == "" || secretKey == "" {
return nil, nil
}
conf := rConfig{
Global: configGlobal{
CattleURL: url,
CattleAccessKey: accessKey,
CattleSecretKey: secretKey,
},
}
return client.NewRancherClient(&client.ClientOpts{
Url: conf.Global.CattleURL,
AccessKey: conf.Global.CattleAccessKey,
SecretKey: conf.Global.CattleSecretKey,
})
}

View File

@ -0,0 +1,120 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rancher_credentials
import (
"path"
"testing"
"github.com/rancher/go-rancher/client"
"k8s.io/kubernetes/pkg/credentialprovider"
)
const username = "foo"
const password = "qwerty"
const email = "foo@bar.baz"
var serverAddresses = []string{"quay.io", "192.168.5.0"}
type testCredentialsGetter struct {
client *client.RancherClient
}
func (p *testCredentialsGetter) getCredentials() []registryCredential {
var registryCreds []registryCredential
for _, serverAddress := range serverAddresses {
cred := &client.RegistryCredential{
PublicValue: username,
SecretValue: password,
Email: email,
}
registryCred := registryCredential{
credential: cred,
serverIP: serverAddress,
}
registryCreds = append(registryCreds, registryCred)
}
return registryCreds
}
func TestRancherCredentialsProvide(t *testing.T) {
image := "foo/bar"
url := "http://localhost:8080"
accessKey := "B481F55E0C48C546E094"
secretKey := "dND2fBcytWWvCRJ8LvqnYcjyNfEkaikvfVxk2C5r"
conf := rConfig{
Global: configGlobal{
CattleURL: url,
CattleAccessKey: accessKey,
CattleSecretKey: secretKey,
},
}
rancherClient, _ := client.NewRancherClient(&client.ClientOpts{
Url: conf.Global.CattleURL,
AccessKey: conf.Global.CattleAccessKey,
SecretKey: conf.Global.CattleSecretKey,
})
testGetter := &testCredentialsGetter{
client: rancherClient,
}
provider := &rancherProvider{
credGetter: testGetter,
}
keyring := &credentialprovider.BasicDockerKeyring{}
keyring.Add(provider.Provide())
for _, registry := range serverAddresses {
fullImagePath := path.Join(registry, image)
creds, ok := keyring.Lookup(fullImagePath)
if !ok {
t.Errorf("Didn't find expected image: %s", fullImagePath)
return
}
if len(creds) > 1 {
t.Errorf("Expected 1 result, received %v", len(creds))
}
val := creds[0]
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
// try to fetch non-existing registry
fullImagePath := path.Join("1.1.1.1", image)
_, ok := keyring.Lookup(fullImagePath)
if ok {
t.Errorf("Found non-existing image: %s", fullImagePath)
}
return
}