mirror of https://github.com/knative/func.git
feat: allow push to cluster internal registries (#718)
* feat: allow push to cluster internal registries Signed-off-by: Matej Vasek <mvasek@redhat.com> * fix: NewRoundTripper consults http.DefaultTransport Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: move credential code to sub-package Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: refactor Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: share RoundTripper avoid creating expensive RoundTripper twice Signed-off-by: Matej Vasek <mvasek@redhat.com> * test: added test for pusher Signed-off-by: Matej Vasek <mvasek@redhat.com> * src: disable parallel layer upload it's more reliable Signed-off-by: Matej Vasek <mvasek@redhat.com> * fixup: lint Signed-off-by: Matej Vasek <mvasek@redhat.com> * fixup: lint Signed-off-by: Matej Vasek <mvasek@redhat.com> * fixup: doc, rm commented code Signed-off-by: Matej Vasek <mvasek@redhat.com>
This commit is contained in:
parent
feaf8f9109
commit
8d51393181
20
cmd/build.go
20
cmd/build.go
|
|
@ -3,6 +3,9 @@ package cmd
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
fnhttp "knative.dev/kn-plugin-func/http"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
|
|
@ -12,6 +15,7 @@ import (
|
|||
fn "knative.dev/kn-plugin-func"
|
||||
"knative.dev/kn-plugin-func/buildpacks"
|
||||
"knative.dev/kn-plugin-func/docker"
|
||||
"knative.dev/kn-plugin-func/docker/creds"
|
||||
"knative.dev/kn-plugin-func/progress"
|
||||
)
|
||||
|
||||
|
|
@ -27,14 +31,14 @@ func newBuildClient(cfg buildConfig) (*fn.Client, error) {
|
|||
|
||||
pusherOption := fn.WithPusher(nil)
|
||||
if cfg.Push {
|
||||
credentialsProvider := docker.NewCredentialsProvider(
|
||||
docker.WithPromptForCredentials(newPromptForCredentials()),
|
||||
docker.WithPromptForCredentialStore(newPromptForCredentialStore()),
|
||||
)
|
||||
credentialsProvider := creds.NewCredentialsProvider(
|
||||
creds.WithPromptForCredentials(newPromptForCredentials()),
|
||||
creds.WithPromptForCredentialStore(newPromptForCredentialStore()),
|
||||
creds.WithTransport(cfg.Transport))
|
||||
pusher, err := docker.NewPusher(
|
||||
docker.WithCredentialsProvider(credentialsProvider),
|
||||
docker.WithProgressListener(listener),
|
||||
)
|
||||
docker.WithTransport(cfg.Transport))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -186,6 +190,10 @@ func runBuild(cmd *cobra.Command, _ []string, clientFn buildClientFn) (err error
|
|||
config.Registry = ""
|
||||
}
|
||||
|
||||
rt := fnhttp.NewRoundTripper()
|
||||
defer rt.Close()
|
||||
config.Transport = rt
|
||||
|
||||
client, err := clientFn(config)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -221,6 +229,8 @@ type buildConfig struct {
|
|||
// with interactive prompting (only applicable when attached to a TTY).
|
||||
Confirm bool
|
||||
Builder string
|
||||
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
func newBuildConfig() buildConfig {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
fnhttp "knative.dev/kn-plugin-func/http"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"github.com/ory/viper"
|
||||
|
|
@ -13,6 +16,7 @@ import (
|
|||
fn "knative.dev/kn-plugin-func"
|
||||
"knative.dev/kn-plugin-func/buildpacks"
|
||||
"knative.dev/kn-plugin-func/docker"
|
||||
"knative.dev/kn-plugin-func/docker/creds"
|
||||
"knative.dev/kn-plugin-func/knative"
|
||||
"knative.dev/kn-plugin-func/progress"
|
||||
)
|
||||
|
|
@ -26,12 +30,14 @@ func newDeployClient(cfg deployConfig) (*fn.Client, error) {
|
|||
|
||||
builder := buildpacks.NewBuilder()
|
||||
|
||||
credentialsProvider := docker.NewCredentialsProvider(
|
||||
docker.WithPromptForCredentials(newPromptForCredentials()),
|
||||
docker.WithPromptForCredentialStore(newPromptForCredentialStore()))
|
||||
credentialsProvider := creds.NewCredentialsProvider(
|
||||
creds.WithPromptForCredentials(newPromptForCredentials()),
|
||||
creds.WithPromptForCredentialStore(newPromptForCredentialStore()),
|
||||
creds.WithTransport(cfg.Transport))
|
||||
pusher, err := docker.NewPusher(
|
||||
docker.WithCredentialsProvider(credentialsProvider),
|
||||
docker.WithProgressListener(listener))
|
||||
docker.WithProgressListener(listener),
|
||||
docker.WithTransport(cfg.Transport))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -174,6 +180,10 @@ func runDeploy(cmd *cobra.Command, _ []string, clientFn deployClientFn) (err err
|
|||
config.Registry = ""
|
||||
}
|
||||
|
||||
rt := fnhttp.NewRoundTripper()
|
||||
defer rt.Close()
|
||||
config.Transport = rt
|
||||
|
||||
client, err := clientFn(config)
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
|
|
@ -228,7 +238,7 @@ func newPromptForCredentials() func(registry string) (docker.Credentials, error)
|
|||
}
|
||||
}
|
||||
|
||||
func newPromptForCredentialStore() docker.ChooseCredentialHelperCallback {
|
||||
func newPromptForCredentialStore() creds.ChooseCredentialHelperCallback {
|
||||
return func(availableHelpers []string) (string, error) {
|
||||
if len(availableHelpers) < 1 {
|
||||
fmt.Fprintf(os.Stderr, `Credentials will not be saved.
|
||||
|
|
@ -284,6 +294,8 @@ type deployConfig struct {
|
|||
|
||||
// Envs passed via cmd to removed
|
||||
EnvToRemove []string
|
||||
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
// newDeployConfig creates a buildConfig populated from command flags and
|
||||
|
|
|
|||
|
|
@ -1,193 +0,0 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/client"
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
var errCredentialsNotFound = errors.New("credentials not found")
|
||||
var errNoCredentialHelperConfigured = errors.New("no credential helper configure")
|
||||
|
||||
func getCredentialHelperFromConfig(confFilePath string) (string, error) {
|
||||
data, err := ioutil.ReadFile(confFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
conf := struct {
|
||||
Store string `json:"credsStore"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &conf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return conf.Store, nil
|
||||
}
|
||||
|
||||
func setCredentialHelperToConfig(confFilePath, helper string) error {
|
||||
var err error
|
||||
|
||||
configData := make(map[string]interface{})
|
||||
|
||||
if data, err := ioutil.ReadFile(confFilePath); err == nil {
|
||||
err = json.Unmarshal(data, &configData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
configData["credsStore"] = helper
|
||||
|
||||
data, err := json.MarshalIndent(&configData, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(confFilePath, data, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCredentialsByCredentialHelper(confFilePath, registry string) (Credentials, error) {
|
||||
result := Credentials{}
|
||||
|
||||
helper, err := getCredentialHelperFromConfig(confFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return result, fmt.Errorf("failed to get helper from config: %w", err)
|
||||
}
|
||||
if helper == "" {
|
||||
return result, errCredentialsNotFound
|
||||
}
|
||||
|
||||
helperName := fmt.Sprintf("docker-credential-%s", helper)
|
||||
p := client.NewShellProgramFunc(helperName)
|
||||
|
||||
credentialsMap, err := client.List(p)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to list credentials: %w", err)
|
||||
}
|
||||
|
||||
for serverUrl := range credentialsMap {
|
||||
if registryEquals(serverUrl, registry) {
|
||||
creds, err := client.Get(p, serverUrl)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to get credentials: %w", err)
|
||||
}
|
||||
result.Username = creds.Username
|
||||
result.Password = creds.Secret
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
return result, fmt.Errorf("failed to get credentials from helper specified in ~/.docker/config.json: %w", errCredentialsNotFound)
|
||||
}
|
||||
|
||||
func setCredentialsByCredentialHelper(confFilePath, registry, username, secret string) error {
|
||||
helper, err := getCredentialHelperFromConfig(confFilePath)
|
||||
|
||||
if helper == "" || os.IsNotExist(err) {
|
||||
return errNoCredentialHelperConfigured
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get helper from config: %w", err)
|
||||
}
|
||||
|
||||
helperName := fmt.Sprintf("docker-credential-%s", helper)
|
||||
p := client.NewShellProgramFunc(helperName)
|
||||
|
||||
return client.Store(p, &credentials.Credentials{ServerURL: registry, Username: username, Secret: secret})
|
||||
}
|
||||
|
||||
func listCredentialHelpers() []string {
|
||||
path := os.Getenv("PATH")
|
||||
paths := strings.Split(path, string(os.PathListSeparator))
|
||||
|
||||
helpers := make(map[string]bool)
|
||||
for _, p := range paths {
|
||||
fss, err := ioutil.ReadDir(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, fi := range fss {
|
||||
if fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(fi.Name(), "docker-credential-") {
|
||||
continue
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
ext := filepath.Ext(fi.Name())
|
||||
if ext != "exe" && ext != "bat" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
helpers[fi.Name()] = true
|
||||
}
|
||||
}
|
||||
result := make([]string, 0, len(helpers))
|
||||
for h := range helpers {
|
||||
result = append(result, h)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func hostPort(registry string) (host string, port string) {
|
||||
host, port = registry, ""
|
||||
if !strings.Contains(registry, "://") {
|
||||
h, p, err := net.SplitHostPort(registry)
|
||||
|
||||
if err == nil {
|
||||
host, port = h, p
|
||||
return
|
||||
}
|
||||
registry = "https://" + registry
|
||||
}
|
||||
|
||||
u, err := url.Parse(registry)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
host = u.Hostname()
|
||||
port = u.Port()
|
||||
return
|
||||
}
|
||||
|
||||
// checks whether registry matches in host and port
|
||||
// with exception where empty port matches standard ports (80,443)
|
||||
func registryEquals(regA, regB string) bool {
|
||||
h1, p1 := hostPort(regA)
|
||||
h2, p2 := hostPort(regB)
|
||||
|
||||
isStdPort := func(p string) bool { return p == "443" || p == "80" }
|
||||
|
||||
portEq := p1 == p2 ||
|
||||
(p1 == "" && isStdPort(p2)) ||
|
||||
(isStdPort(p1) && p2 == "")
|
||||
|
||||
if h1 == h2 && portEq {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasSuffix(h1, "docker.io") &&
|
||||
strings.HasSuffix(h2, "docker.io") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
//go:build !integration
|
||||
// +build !integration
|
||||
|
||||
package docker
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_registryEquals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
urlA string
|
||||
urlB string
|
||||
want bool
|
||||
}{
|
||||
{"no port matching host", "quay.io", "quay.io", true},
|
||||
{"non-matching host added sub-domain", "sub.quay.io", "quay.io", false},
|
||||
{"non-matching host different sub-domain", "sub.quay.io", "sub3.quay.io", false},
|
||||
{"localhost", "localhost", "localhost", true},
|
||||
{"localhost with standard ports", "localhost:80", "localhost:443", false},
|
||||
{"localhost with matching port", "https://localhost:1234", "http://localhost:1234", true},
|
||||
{"localhost with match by default port 80", "http://localhost", "localhost:80", true},
|
||||
{"localhost with match by default port 443", "https://localhost", "localhost:443", true},
|
||||
{"localhost with mismatch by non-default port 5000", "https://localhost", "localhost:5000", false},
|
||||
{"localhost with match by empty ports", "https://localhost", "http://localhost", true},
|
||||
{"docker.io matching host https", "https://docker.io", "docker.io", true},
|
||||
{"docker.io matching host http", "http://docker.io", "docker.io", true},
|
||||
{"docker.io with path", "docker.io/v1/", "docker.io", true},
|
||||
{"docker.io with protocol and path", "https://docker.io/v1/", "docker.io", true},
|
||||
{"docker.io with subdomain index.", "https://index.docker.io/v1/", "docker.io", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := registryEquals(tt.urlA, tt.urlB); got != tt.want {
|
||||
t.Errorf("to2ndLevelDomain() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,451 @@
|
|||
package creds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
"knative.dev/kn-plugin-func/docker"
|
||||
|
||||
"github.com/containers/image/v5/pkg/docker/config"
|
||||
containersTypes "github.com/containers/image/v5/types"
|
||||
"github.com/docker/docker-credential-helpers/client"
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
)
|
||||
|
||||
type CredentialsCallback func(registry string) (docker.Credentials, error)
|
||||
|
||||
var ErrUnauthorized = errors.New("bad credentials")
|
||||
|
||||
// VerifyCredentialsCallback checks if credentials are accepted by the registry.
|
||||
// If credentials are incorrect this callback shall return ErrUnauthorized.
|
||||
type VerifyCredentialsCallback func(ctx context.Context, registry string, credentials docker.Credentials) error
|
||||
|
||||
// CheckAuth verifies that credentials are correct
|
||||
func CheckAuth(ctx context.Context, registry string, credentials docker.Credentials, trans http.RoundTripper) error {
|
||||
serverAddress := registry
|
||||
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
|
||||
serverAddress = "https://" + serverAddress
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2", serverAddress)
|
||||
|
||||
authenticator := &authn.Basic{
|
||||
Username: credentials.Username,
|
||||
Password: credentials.Password,
|
||||
}
|
||||
|
||||
reg, err := name.NewRegistry(registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tr, err := transport.NewWithContext(ctx, reg, authenticator, trans, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli := http.Client{Transport: tr}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify credentials: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch {
|
||||
case resp.StatusCode == http.StatusUnauthorized:
|
||||
return ErrUnauthorized
|
||||
case resp.StatusCode != http.StatusOK:
|
||||
return fmt.Errorf("failed to verify credentials: status code: %d", resp.StatusCode)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type ChooseCredentialHelperCallback func(available []string) (string, error)
|
||||
|
||||
type credentialsProvider struct {
|
||||
promptForCredentials CredentialsCallback
|
||||
verifyCredentials VerifyCredentialsCallback
|
||||
promptForCredentialStore ChooseCredentialHelperCallback
|
||||
credentialLoaders []CredentialsCallback
|
||||
authFilePath string
|
||||
transport http.RoundTripper
|
||||
}
|
||||
|
||||
type Opt func(opts *credentialsProvider)
|
||||
|
||||
// WithPromptForCredentials sets custom callback that is supposed to
|
||||
// interactively ask for credentials in case the credentials cannot be found in configuration files.
|
||||
// The callback may be called multiple times in case incorrect credentials were returned before.
|
||||
func WithPromptForCredentials(cbk CredentialsCallback) Opt {
|
||||
return func(opts *credentialsProvider) {
|
||||
opts.promptForCredentials = cbk
|
||||
}
|
||||
}
|
||||
|
||||
// WithVerifyCredentials sets custom callback for credentials validation.
|
||||
func WithVerifyCredentials(cbk VerifyCredentialsCallback) Opt {
|
||||
return func(opts *credentialsProvider) {
|
||||
opts.verifyCredentials = cbk
|
||||
}
|
||||
}
|
||||
|
||||
// WithPromptForCredentialStore sets custom callback that is supposed to
|
||||
// interactively ask user which credentials store/helper is used to store credentials obtained
|
||||
// from user.
|
||||
func WithPromptForCredentialStore(cbk ChooseCredentialHelperCallback) Opt {
|
||||
return func(opts *credentialsProvider) {
|
||||
opts.promptForCredentialStore = cbk
|
||||
}
|
||||
}
|
||||
|
||||
func WithTransport(transport http.RoundTripper) Opt {
|
||||
return func(opts *credentialsProvider) {
|
||||
opts.transport = transport
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditionalCredentialLoaders adds custom callbacks for credential retrieval.
|
||||
// The callbacks are supposed to be non-interactive as opposed to WithPromptForCredentials.
|
||||
//
|
||||
// This might be useful when credentials are shared with some other service.
|
||||
//
|
||||
// Example: OpenShift builtin registry shares credentials with the cluster (k8s) credentials.
|
||||
func WithAdditionalCredentialLoaders(loaders ...CredentialsCallback) Opt {
|
||||
return func(opts *credentialsProvider) {
|
||||
opts.credentialLoaders = append(opts.credentialLoaders, loaders...)
|
||||
}
|
||||
}
|
||||
|
||||
// NewCredentialsProvider returns new CredentialsProvider that tries to get credentials from docker/func config files.
|
||||
//
|
||||
// In case getting credentials from the config files fails
|
||||
// the caller provided callback (see WithPromptForCredentials) will be invoked to obtain credentials.
|
||||
// The callback may be called multiple times in case the returned credentials
|
||||
// are not correct (see WithVerifyCredentials).
|
||||
//
|
||||
// When the callback succeeds the credentials will be saved by using helper defined in the func config.
|
||||
// If the helper is not defined in the config file
|
||||
// it may be picked by provided callback (see WithPromptForCredentialStore).
|
||||
// The picked value will be saved in the func config.
|
||||
//
|
||||
// To verify that credentials are correct custom callback can be used (see WithVerifyCredentials).
|
||||
func NewCredentialsProvider(opts ...Opt) docker.CredentialsProvider {
|
||||
var c credentialsProvider
|
||||
|
||||
for _, o := range opts {
|
||||
o(&c)
|
||||
}
|
||||
|
||||
if c.transport == nil {
|
||||
c.transport = http.DefaultTransport
|
||||
}
|
||||
|
||||
if c.verifyCredentials == nil {
|
||||
c.verifyCredentials = func(ctx context.Context, registry string, credentials docker.Credentials) error {
|
||||
return CheckAuth(ctx, registry, credentials, c.transport)
|
||||
}
|
||||
}
|
||||
|
||||
if c.promptForCredentialStore == nil {
|
||||
c.promptForCredentialStore = func(available []string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
c.authFilePath = filepath.Join(fn.ConfigPath(), "auth.json")
|
||||
sys := &containersTypes.SystemContext{
|
||||
AuthFilePath: c.authFilePath,
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dockerConfigPath := filepath.Join(home, ".docker", "config.json")
|
||||
|
||||
var defaultCredentialLoaders = []CredentialsCallback{
|
||||
func(registry string) (docker.Credentials, error) {
|
||||
creds, err := config.GetCredentials(sys, registry)
|
||||
if err != nil {
|
||||
return docker.Credentials{}, err
|
||||
}
|
||||
return docker.Credentials{
|
||||
Username: creds.Username,
|
||||
Password: creds.Password,
|
||||
}, nil
|
||||
},
|
||||
func(registry string) (docker.Credentials, error) {
|
||||
return getCredentialsByCredentialHelper(c.authFilePath, registry)
|
||||
},
|
||||
func(registry string) (docker.Credentials, error) {
|
||||
return getCredentialsByCredentialHelper(dockerConfigPath, registry)
|
||||
},
|
||||
}
|
||||
|
||||
c.credentialLoaders = append(c.credentialLoaders, defaultCredentialLoaders...)
|
||||
|
||||
return c.getCredentials
|
||||
}
|
||||
|
||||
func (c *credentialsProvider) getCredentials(ctx context.Context, registry string) (docker.Credentials, error) {
|
||||
var err error
|
||||
result := docker.Credentials{}
|
||||
|
||||
for _, load := range c.credentialLoaders {
|
||||
|
||||
result, err = load(registry)
|
||||
|
||||
if err != nil && !errors.Is(err, errCredentialsNotFound) {
|
||||
return docker.Credentials{}, err
|
||||
}
|
||||
|
||||
if result != (docker.Credentials{}) {
|
||||
err = c.verifyCredentials(ctx, registry, result)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
} else {
|
||||
if !errors.Is(err, ErrUnauthorized) {
|
||||
return docker.Credentials{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.promptForCredentials == nil {
|
||||
return docker.Credentials{}, errCredentialsNotFound
|
||||
}
|
||||
|
||||
for {
|
||||
result, err = c.promptForCredentials(registry)
|
||||
if err != nil {
|
||||
return docker.Credentials{}, err
|
||||
}
|
||||
|
||||
err = c.verifyCredentials(ctx, registry, result)
|
||||
if err == nil {
|
||||
err = setCredentialsByCredentialHelper(c.authFilePath, registry, result.Username, result.Password)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errNoCredentialHelperConfigured) {
|
||||
return docker.Credentials{}, err
|
||||
}
|
||||
helpers := listCredentialHelpers()
|
||||
helper, err := c.promptForCredentialStore(helpers)
|
||||
if err != nil {
|
||||
return docker.Credentials{}, err
|
||||
}
|
||||
helper = strings.TrimPrefix(helper, "docker-credential-")
|
||||
err = setCredentialHelperToConfig(c.authFilePath, helper)
|
||||
if err != nil {
|
||||
return docker.Credentials{}, fmt.Errorf("faild to set the helper to the config: %w", err)
|
||||
}
|
||||
err = setCredentialsByCredentialHelper(c.authFilePath, registry, result.Username, result.Password)
|
||||
if err != nil && !errors.Is(err, errNoCredentialHelperConfigured) {
|
||||
return docker.Credentials{}, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
} else {
|
||||
if errors.Is(err, ErrUnauthorized) {
|
||||
continue
|
||||
}
|
||||
return docker.Credentials{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errCredentialsNotFound = errors.New("credentials not found")
|
||||
var errNoCredentialHelperConfigured = errors.New("no credential helper configure")
|
||||
|
||||
func getCredentialHelperFromConfig(confFilePath string) (string, error) {
|
||||
data, err := ioutil.ReadFile(confFilePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
conf := struct {
|
||||
Store string `json:"credsStore"`
|
||||
}{}
|
||||
|
||||
err = json.Unmarshal(data, &conf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return conf.Store, nil
|
||||
}
|
||||
|
||||
func setCredentialHelperToConfig(confFilePath, helper string) error {
|
||||
var err error
|
||||
|
||||
configData := make(map[string]interface{})
|
||||
|
||||
if data, err := ioutil.ReadFile(confFilePath); err == nil {
|
||||
err = json.Unmarshal(data, &configData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
configData["credsStore"] = helper
|
||||
|
||||
data, err := json.MarshalIndent(&configData, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(confFilePath, data, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCredentialsByCredentialHelper(confFilePath, registry string) (docker.Credentials, error) {
|
||||
result := docker.Credentials{}
|
||||
|
||||
helper, err := getCredentialHelperFromConfig(confFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return result, fmt.Errorf("failed to get helper from config: %w", err)
|
||||
}
|
||||
if helper == "" {
|
||||
return result, errCredentialsNotFound
|
||||
}
|
||||
|
||||
helperName := fmt.Sprintf("docker-credential-%s", helper)
|
||||
p := client.NewShellProgramFunc(helperName)
|
||||
|
||||
credentialsMap, err := client.List(p)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to list credentials: %w", err)
|
||||
}
|
||||
|
||||
for serverUrl := range credentialsMap {
|
||||
if RegistryEquals(serverUrl, registry) {
|
||||
creds, err := client.Get(p, serverUrl)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to get credentials: %w", err)
|
||||
}
|
||||
result.Username = creds.Username
|
||||
result.Password = creds.Secret
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
return result, fmt.Errorf("failed to get credentials from helper specified in ~/.docker/config.json: %w", errCredentialsNotFound)
|
||||
}
|
||||
|
||||
func setCredentialsByCredentialHelper(confFilePath, registry, username, secret string) error {
|
||||
helper, err := getCredentialHelperFromConfig(confFilePath)
|
||||
|
||||
if helper == "" || os.IsNotExist(err) {
|
||||
return errNoCredentialHelperConfigured
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get helper from config: %w", err)
|
||||
}
|
||||
|
||||
helperName := fmt.Sprintf("docker-credential-%s", helper)
|
||||
p := client.NewShellProgramFunc(helperName)
|
||||
|
||||
return client.Store(p, &credentials.Credentials{ServerURL: registry, Username: username, Secret: secret})
|
||||
}
|
||||
|
||||
func listCredentialHelpers() []string {
|
||||
path := os.Getenv("PATH")
|
||||
paths := strings.Split(path, string(os.PathListSeparator))
|
||||
|
||||
helpers := make(map[string]bool)
|
||||
for _, p := range paths {
|
||||
fss, err := ioutil.ReadDir(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, fi := range fss {
|
||||
if fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(fi.Name(), "docker-credential-") {
|
||||
continue
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
ext := filepath.Ext(fi.Name())
|
||||
if ext != "exe" && ext != "bat" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
helpers[fi.Name()] = true
|
||||
}
|
||||
}
|
||||
result := make([]string, 0, len(helpers))
|
||||
for h := range helpers {
|
||||
result = append(result, h)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func hostPort(registry string) (host string, port string) {
|
||||
host, port = registry, ""
|
||||
if !strings.Contains(registry, "://") {
|
||||
h, p, err := net.SplitHostPort(registry)
|
||||
|
||||
if err == nil {
|
||||
host, port = h, p
|
||||
return
|
||||
}
|
||||
registry = "https://" + registry
|
||||
}
|
||||
|
||||
u, err := url.Parse(registry)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
host = u.Hostname()
|
||||
port = u.Port()
|
||||
return
|
||||
}
|
||||
|
||||
// RegistryEquals checks whether registry matches in host and port
|
||||
// with exception where empty port matches standard ports (80,443)
|
||||
func RegistryEquals(regA, regB string) bool {
|
||||
h1, p1 := hostPort(regA)
|
||||
h2, p2 := hostPort(regB)
|
||||
|
||||
isStdPort := func(p string) bool { return p == "443" || p == "80" }
|
||||
|
||||
portEq := p1 == p2 ||
|
||||
(p1 == "" && isStdPort(p2)) ||
|
||||
(isStdPort(p1) && p2 == "")
|
||||
|
||||
if h1 == h2 && portEq {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasSuffix(h1, "docker.io") &&
|
||||
strings.HasSuffix(h2, "docker.io") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,870 @@
|
|||
//go:build !integration
|
||||
// +build !integration
|
||||
|
||||
package creds_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
"knative.dev/kn-plugin-func/docker"
|
||||
"knative.dev/kn-plugin-func/docker/creds"
|
||||
. "knative.dev/kn-plugin-func/testing"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
func Test_registryEquals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
urlA string
|
||||
urlB string
|
||||
want bool
|
||||
}{
|
||||
{"no port matching host", "quay.io", "quay.io", true},
|
||||
{"non-matching host added sub-domain", "sub.quay.io", "quay.io", false},
|
||||
{"non-matching host different sub-domain", "sub.quay.io", "sub3.quay.io", false},
|
||||
{"localhost", "localhost", "localhost", true},
|
||||
{"localhost with standard ports", "localhost:80", "localhost:443", false},
|
||||
{"localhost with matching port", "https://localhost:1234", "http://localhost:1234", true},
|
||||
{"localhost with match by default port 80", "http://localhost", "localhost:80", true},
|
||||
{"localhost with match by default port 443", "https://localhost", "localhost:443", true},
|
||||
{"localhost with mismatch by non-default port 5000", "https://localhost", "localhost:5000", false},
|
||||
{"localhost with match by empty ports", "https://localhost", "http://localhost", true},
|
||||
{"docker.io matching host https", "https://docker.io", "docker.io", true},
|
||||
{"docker.io matching host http", "http://docker.io", "docker.io", true},
|
||||
{"docker.io with path", "docker.io/v1/", "docker.io", true},
|
||||
{"docker.io with protocol and path", "https://docker.io/v1/", "docker.io", true},
|
||||
{"docker.io with subdomain index.", "https://index.docker.io/v1/", "docker.io", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := creds.RegistryEquals(tt.urlA, tt.urlB); got != tt.want {
|
||||
t.Errorf("to2ndLevelDomain() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAuth(t *testing.T) {
|
||||
localhost, localhostTLS, stopServer := startServer(t)
|
||||
defer stopServer()
|
||||
|
||||
_, portTLS, err := net.SplitHostPort(localhostTLS)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nonLocalhostTLS := "test.io:" + portTLS
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
username string
|
||||
password string
|
||||
registry string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "correct credentials localhost no-TLS",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
username: "testuser",
|
||||
password: "testpwd",
|
||||
registry: localhost,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "correct credentials localhost",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
username: "testuser",
|
||||
password: "testpwd",
|
||||
registry: localhostTLS,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "correct credentials non-localhost",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
username: "testuser",
|
||||
password: "testpwd",
|
||||
registry: nonLocalhostTLS,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "incorrect credentials localhost no-TLS",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
username: "testuser",
|
||||
password: "badpwd",
|
||||
registry: localhost,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "incorrect credentials localhost",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
username: "testuser",
|
||||
password: "badpwd",
|
||||
registry: localhostTLS,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := docker.Credentials{
|
||||
Username: tt.args.username,
|
||||
Password: tt.args.password,
|
||||
}
|
||||
if err := creds.CheckAuth(tt.args.ctx, tt.args.registry, c, http.DefaultTransport); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckAuth() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func startServer(t *testing.T) (addr, addrTLS string, stopServer func()) {
|
||||
listener, err := net.Listen("tcp", "localhost:8080")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addr = listener.Addr().String()
|
||||
|
||||
listenerTLS, err := net.Listen("tcp", "localhost:4433")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addrTLS = listenerTLS.Addr().String()
|
||||
|
||||
handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
resp.Header().Add("WWW-Authenticate", "basic")
|
||||
if user, pwd, ok := req.BasicAuth(); ok {
|
||||
if user == "testuser" && pwd == "testpwd" {
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
resp.WriteHeader(http.StatusUnauthorized)
|
||||
})
|
||||
|
||||
var randReader io.Reader = rand.Reader
|
||||
|
||||
caPublicKey, caPrivateKey, err := ed25519.GenerateKey(randReader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ca := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
CommonName: "localhost",
|
||||
},
|
||||
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
|
||||
DNSNames: []string{"localhost", "test.io"},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(10, 0, 0),
|
||||
IsCA: true,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||||
ExtraExtensions: []pkix.Extension{},
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
caBytes, err := x509.CreateCertificate(randReader, ca, ca, caPublicKey, caPrivateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ca, err = x509.ParseCertificate(caBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cert := tls.Certificate{
|
||||
Certificate: [][]byte{caBytes},
|
||||
PrivateKey: caPrivateKey,
|
||||
Leaf: ca,
|
||||
}
|
||||
|
||||
server := http.Server{
|
||||
Handler: handler,
|
||||
TLSConfig: &tls.Config{
|
||||
ServerName: "localhost",
|
||||
Certificates: []tls.Certificate{cert},
|
||||
},
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := server.ServeTLS(listenerTLS, "", "")
|
||||
if err != nil && !strings.Contains(err.Error(), "Server closed") {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
if err != nil && !strings.Contains(err.Error(), "Server closed") {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// make the testing CA trusted by default HTTP transport/client
|
||||
oldDefaultTransport := http.DefaultTransport
|
||||
newDefaultTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
http.DefaultTransport = newDefaultTransport
|
||||
caPool := x509.NewCertPool()
|
||||
caPool.AddCert(ca)
|
||||
newDefaultTransport.TLSClientConfig.RootCAs = caPool
|
||||
dc := newDefaultTransport.DialContext
|
||||
newDefaultTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
h, p, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h == "test.io" {
|
||||
h = "localhost"
|
||||
}
|
||||
addr = net.JoinHostPort(h, p)
|
||||
return dc(ctx, network, addr)
|
||||
}
|
||||
|
||||
return addr, addrTLS, func() {
|
||||
err := server.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
http.DefaultTransport = oldDefaultTransport
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
dockerIoUser = "testUser1"
|
||||
dockerIoUserPwd = "goodPwd1"
|
||||
quayIoUser = "testUser2"
|
||||
quayIoUserPwd = "goodPwd2"
|
||||
)
|
||||
|
||||
type Credentials = docker.Credentials
|
||||
|
||||
func TestNewCredentialsProvider(t *testing.T) {
|
||||
defer withCleanHome(t)()
|
||||
|
||||
helperWithQuayIO := newInMemoryHelper()
|
||||
|
||||
err := helperWithQuayIO.Add(&credentials.Credentials{
|
||||
ServerURL: "quay.io",
|
||||
Username: quayIoUser,
|
||||
Secret: quayIoUserPwd,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type args struct {
|
||||
promptUser creds.CredentialsCallback
|
||||
verifyCredentials creds.VerifyCredentialsCallback
|
||||
additionalLoaders []creds.CredentialsCallback
|
||||
registry string
|
||||
setUpEnv setUpEnv
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Credentials
|
||||
}{
|
||||
{
|
||||
name: "test user callback correct password on first try",
|
||||
args: args{
|
||||
promptUser: correctPwdCallback,
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "test user callback correct password on second try",
|
||||
args: args{
|
||||
promptUser: pwdCbkFirstWrongThenCorrect(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get quay-io credentials with func config populated",
|
||||
args: args{
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "quay.io",
|
||||
setUpEnv: withPopulatedFuncAuthConfig,
|
||||
},
|
||||
want: Credentials{Username: quayIoUser, Password: quayIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get docker-io credentials with func config populated",
|
||||
args: args{
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
setUpEnv: withPopulatedFuncAuthConfig,
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get quay-io credentials with docker config populated",
|
||||
args: args{
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "quay.io",
|
||||
setUpEnv: all(
|
||||
withPopulatedDockerAuthConfig,
|
||||
setUpMockHelper("docker-credential-mock", helperWithQuayIO)),
|
||||
},
|
||||
want: Credentials{Username: quayIoUser, Password: quayIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get docker-io credentials with docker config populated",
|
||||
args: args{
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
setUpEnv: withPopulatedDockerAuthConfig,
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
{
|
||||
name: "get docker-io credentials from custom loader",
|
||||
args: args{
|
||||
promptUser: pwdCbkThatShallNotBeCalled(t),
|
||||
verifyCredentials: correctVerifyCbk,
|
||||
registry: "docker.io",
|
||||
additionalLoaders: []creds.CredentialsCallback{correctPwdCallback},
|
||||
},
|
||||
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer cleanUpConfigs(t)
|
||||
|
||||
if tt.args.setUpEnv != nil {
|
||||
defer tt.args.setUpEnv(t)()
|
||||
}
|
||||
|
||||
credentialsProvider := creds.NewCredentialsProvider(
|
||||
creds.WithPromptForCredentials(tt.args.promptUser),
|
||||
creds.WithVerifyCredentials(tt.args.verifyCredentials),
|
||||
creds.WithAdditionalCredentialLoaders(tt.args.additionalLoaders...))
|
||||
got, err := credentialsProvider(context.Background(), tt.args.registry)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("credentialsProvider() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialsProviderSavingFromUserInput(t *testing.T) {
|
||||
defer withCleanHome(t)()
|
||||
|
||||
helper := newInMemoryHelper()
|
||||
defer setUpMockHelper("docker-credential-mock", helper)(t)()
|
||||
|
||||
var pwdCbkInvocations int
|
||||
pwdCbk := func(r string) (Credentials, error) {
|
||||
pwdCbkInvocations++
|
||||
return correctPwdCallback(r)
|
||||
}
|
||||
|
||||
chooseNoStore := func(available []string) (string, error) {
|
||||
if len(available) < 1 {
|
||||
t.Errorf("this should have been invoked with non empty list")
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
chooseMockStore := func(available []string) (string, error) {
|
||||
if len(available) < 1 {
|
||||
t.Errorf("this should have been invoked with non empty list")
|
||||
}
|
||||
return "docker-credential-mock", nil
|
||||
}
|
||||
shallNotBeInvoked := func(available []string) (string, error) {
|
||||
t.Fatal("this choose helper callback shall not be invoked")
|
||||
return "", errors.New("this callback shall not be invoked")
|
||||
}
|
||||
|
||||
credentialsProvider := creds.NewCredentialsProvider(
|
||||
creds.WithPromptForCredentials(pwdCbk),
|
||||
creds.WithVerifyCredentials(correctVerifyCbk),
|
||||
creds.WithPromptForCredentialStore(chooseNoStore))
|
||||
_, err := credentialsProvider(context.Background(), "docker.io")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// now credentials should not be saved because no helper was provided
|
||||
l, err := helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credsInStore := len(l)
|
||||
if credsInStore != 0 {
|
||||
t.Errorf("expected to have zero credentials in store, but has: %d", credsInStore)
|
||||
}
|
||||
credentialsProvider = creds.NewCredentialsProvider(
|
||||
creds.WithPromptForCredentials(pwdCbk),
|
||||
creds.WithVerifyCredentials(correctVerifyCbk),
|
||||
creds.WithPromptForCredentialStore(chooseMockStore))
|
||||
_, err = credentialsProvider(context.Background(), "docker.io")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
if pwdCbkInvocations != 2 {
|
||||
t.Errorf("the pwd callback should have been invoked exactly twice but was invoked %d time", pwdCbkInvocations)
|
||||
}
|
||||
|
||||
// now credentials should be saved in the mock secure store
|
||||
l, err = helper.List()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
credsInStore = len(l)
|
||||
if len(l) != 1 {
|
||||
t.Errorf("expected to have exactly one credentials in store, but has: %d", credsInStore)
|
||||
}
|
||||
credentialsProvider = creds.NewCredentialsProvider(
|
||||
creds.WithPromptForCredentials(pwdCbkThatShallNotBeCalled(t)),
|
||||
creds.WithVerifyCredentials(correctVerifyCbk),
|
||||
creds.WithPromptForCredentialStore(shallNotBeInvoked))
|
||||
_, err = credentialsProvider(context.Background(), "docker.io")
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func cleanUpConfigs(t *testing.T) {
|
||||
home, err := os.Hostname()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
os.RemoveAll(fn.ConfigPath())
|
||||
|
||||
os.RemoveAll(filepath.Join(home, ".docker"))
|
||||
}
|
||||
|
||||
type setUpEnv = func(t *testing.T) func()
|
||||
|
||||
func withPopulatedDockerAuthConfig(t *testing.T) func() {
|
||||
t.Helper()
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dockerConfigDir := filepath.Join(home, ".docker")
|
||||
dockerConfigPath := filepath.Join(dockerConfigDir, "config.json")
|
||||
err = os.MkdirAll(filepath.Dir(dockerConfigPath), 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
configJSON := `{
|
||||
"auths": {
|
||||
"docker.io": { "auth": "%s" },
|
||||
"quay.io": {}
|
||||
},
|
||||
"credsStore": "mock"
|
||||
}`
|
||||
configJSON = fmt.Sprintf(configJSON, base64.StdEncoding.EncodeToString([]byte(dockerIoUser+":"+dockerIoUserPwd)))
|
||||
|
||||
err = ioutil.WriteFile(dockerConfigPath, []byte(configJSON), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return func() {
|
||||
|
||||
os.RemoveAll(dockerConfigDir)
|
||||
}
|
||||
}
|
||||
|
||||
func withPopulatedFuncAuthConfig(t *testing.T) func() {
|
||||
t.Helper()
|
||||
|
||||
var err error
|
||||
|
||||
authConfig := filepath.Join(fn.ConfigPath(), "auth.json")
|
||||
err = os.MkdirAll(filepath.Dir(authConfig), 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
authJSON := `{
|
||||
"auths": {
|
||||
"docker.io": { "auth": "%s" },
|
||||
"quay.io": { "auth": "%s" }
|
||||
}
|
||||
}`
|
||||
authJSON = fmt.Sprintf(authJSON,
|
||||
base64.StdEncoding.EncodeToString([]byte(dockerIoUser+":"+dockerIoUserPwd)),
|
||||
base64.StdEncoding.EncodeToString([]byte(quayIoUser+":"+quayIoUserPwd)))
|
||||
|
||||
err = ioutil.WriteFile(authConfig, []byte(authJSON), 0600)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return func() {
|
||||
os.RemoveAll(fn.ConfigPath())
|
||||
}
|
||||
}
|
||||
|
||||
func pwdCbkThatShallNotBeCalled(t *testing.T) creds.CredentialsCallback {
|
||||
t.Helper()
|
||||
return func(registry string) (Credentials, error) {
|
||||
return Credentials{}, errors.New("this pwd cbk code shall not be called")
|
||||
}
|
||||
}
|
||||
|
||||
func pwdCbkFirstWrongThenCorrect(t *testing.T) func(registry string) (Credentials, error) {
|
||||
t.Helper()
|
||||
var firstInvocation bool
|
||||
return func(registry string) (Credentials, error) {
|
||||
if registry != "docker.io" && registry != "quay.io" {
|
||||
return Credentials{}, fmt.Errorf("unexpected registry: %s", registry)
|
||||
}
|
||||
if firstInvocation {
|
||||
firstInvocation = false
|
||||
return Credentials{Username: dockerIoUser, Password: "badPwd"}, nil
|
||||
}
|
||||
return correctPwdCallback(registry)
|
||||
}
|
||||
}
|
||||
|
||||
func correctPwdCallback(registry string) (Credentials, error) {
|
||||
if registry == "docker.io" {
|
||||
return Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, nil
|
||||
}
|
||||
if registry == "quay.io" {
|
||||
return Credentials{Username: quayIoUser, Password: quayIoUserPwd}, nil
|
||||
}
|
||||
return Credentials{}, errors.New("this cbk don't know the pwd")
|
||||
}
|
||||
|
||||
func correctVerifyCbk(ctx context.Context, registry string, credentials Credentials) error {
|
||||
username, password := credentials.Username, credentials.Password
|
||||
if username == dockerIoUser && password == dockerIoUserPwd && registry == "docker.io" {
|
||||
return nil
|
||||
}
|
||||
if username == quayIoUser && password == quayIoUserPwd && registry == "quay.io" {
|
||||
return nil
|
||||
}
|
||||
return creds.ErrUnauthorized
|
||||
}
|
||||
|
||||
func withCleanHome(t *testing.T) func() {
|
||||
t.Helper()
|
||||
homeName := "HOME"
|
||||
if runtime.GOOS == "windows" {
|
||||
homeName = "USERPROFILE"
|
||||
}
|
||||
tmpHome := t.TempDir()
|
||||
oldHome, hadHome := os.LookupEnv(homeName)
|
||||
os.Setenv(homeName, tmpHome)
|
||||
|
||||
oldXDGConfigHome, hadXDGConfigHome := os.LookupEnv("XDG_CONFIG_HOME")
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
os.Setenv("XDG_CONFIG_HOME", filepath.Join(tmpHome, ".config"))
|
||||
}
|
||||
|
||||
return func() {
|
||||
if hadHome {
|
||||
os.Setenv(homeName, oldHome)
|
||||
} else {
|
||||
os.Unsetenv(homeName)
|
||||
}
|
||||
if hadXDGConfigHome {
|
||||
os.Setenv("XDG_CONFIG_HOME", oldXDGConfigHome)
|
||||
} else {
|
||||
os.Unsetenv("XDG_CONFIG_HOME")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handlerForCredHelper(t *testing.T, credHelper credentials.Helper) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
defer request.Body.Close()
|
||||
|
||||
var err error
|
||||
var outBody interface{}
|
||||
|
||||
uri := strings.Trim(request.RequestURI, "/")
|
||||
|
||||
var serverURL string
|
||||
if uri == "get" || uri == "erase" {
|
||||
data, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
serverURL = string(data)
|
||||
serverURL = strings.Trim(serverURL, "\n\r\t ")
|
||||
}
|
||||
|
||||
switch uri {
|
||||
case "list":
|
||||
var list map[string]string
|
||||
list, err = credHelper.List()
|
||||
if err == nil {
|
||||
outBody = &list
|
||||
}
|
||||
case "store":
|
||||
creds := credentials.Credentials{}
|
||||
dec := json.NewDecoder(request.Body)
|
||||
err = dec.Decode(&creds)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
err = credHelper.Add(&creds)
|
||||
case "get":
|
||||
var user, secret string
|
||||
user, secret, err = credHelper.Get(serverURL)
|
||||
if err == nil {
|
||||
outBody = &credentials.Credentials{ServerURL: serverURL, Username: user, Secret: secret}
|
||||
}
|
||||
case "erase":
|
||||
err = credHelper.Delete(serverURL)
|
||||
default:
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if credentials.IsErrCredentialsNotFound(err) {
|
||||
writer.WriteHeader(http.StatusNotFound)
|
||||
} else {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
writer.Header().Add("Content-Type", "text/plain")
|
||||
fmt.Fprintf(writer, "error: %+v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if outBody != nil {
|
||||
var data []byte
|
||||
data, err = json.Marshal(outBody)
|
||||
if err != nil {
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writer.Header().Add("Content-Type", "application/json")
|
||||
_, err = writer.Write(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Go source code of mock docker-credential-helper implementation.
|
||||
// Its storage is backed by inMemoryHelper instantiated in test and exposed via HTTP.
|
||||
const helperGoSrc = `package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
baseURL = os.Getenv("HELPER_BASE_URL")
|
||||
resp *http.Response
|
||||
err error
|
||||
)
|
||||
cmd := os.Args[1]
|
||||
switch cmd {
|
||||
case "list":
|
||||
resp, err = http.Get(baseURL + "/" + cmd)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
io.Copy(os.Stdout, resp.Body)
|
||||
case "get", "erase":
|
||||
resp, err = http.Post(baseURL+ "/" + cmd, "text/plain", os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
io.Copy(os.Stdout, resp.Body)
|
||||
case "store":
|
||||
resp, err = http.Post(baseURL+ "/" + cmd, "application/json", os.Stdin)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
default:
|
||||
log.Fatal(errors.New("unknown cmd: " + cmd))
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Fatal(errors.New(resp.Status))
|
||||
}
|
||||
return
|
||||
}
|
||||
`
|
||||
|
||||
// Creates executable with name determined by the helperName parameter and puts it on $PATH.
|
||||
//
|
||||
// The executable behaves like docker credential helper (https://github.com/docker/docker-credential-helpers).
|
||||
//
|
||||
// The content of the store presented by the executable is backed by the helper parameter.
|
||||
func setUpMockHelper(helperName string, helper credentials.Helper) func(t *testing.T) func() {
|
||||
var cleanUps []func()
|
||||
return func(t *testing.T) func() {
|
||||
|
||||
cleanUps = append(cleanUps, WithExecutable(t, helperName, helperGoSrc))
|
||||
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanUps = append(cleanUps, func() {
|
||||
_ = listener.Close()
|
||||
})
|
||||
|
||||
baseURL := fmt.Sprintf("http://%s", listener.Addr().String())
|
||||
cleanUps = append(cleanUps, WithEnvVar(t, "HELPER_BASE_URL", baseURL))
|
||||
|
||||
server := http.Server{Handler: handlerForCredHelper(t, helper)}
|
||||
servErrChan := make(chan error)
|
||||
go func() {
|
||||
servErrChan <- server.Serve(listener)
|
||||
}()
|
||||
|
||||
cleanUps = append(cleanUps, func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
_ = server.Shutdown(ctx)
|
||||
e := <-servErrChan
|
||||
if !errors.Is(e, http.ErrServerClosed) {
|
||||
t.Fatal(e)
|
||||
}
|
||||
})
|
||||
|
||||
return func() {
|
||||
for i := len(cleanUps) - 1; i <= 0; i-- {
|
||||
cleanUps[i]()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// combines multiple setUp routines into one setUp routine
|
||||
func all(fns ...setUpEnv) setUpEnv {
|
||||
return func(t *testing.T) func() {
|
||||
t.Helper()
|
||||
var cleanUps []func()
|
||||
for _, fn := range fns {
|
||||
cleanUps = append(cleanUps, fn(t))
|
||||
}
|
||||
|
||||
return func() {
|
||||
for i := len(cleanUps) - 1; i >= 0; i-- {
|
||||
cleanUps[i]()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newInMemoryHelper() *inMemoryHelper {
|
||||
return &inMemoryHelper{lock: &sync.Mutex{}, credentials: make(map[string]credentials.Credentials)}
|
||||
}
|
||||
|
||||
type inMemoryHelper struct {
|
||||
credentials map[string]credentials.Credentials
|
||||
lock sync.Locker
|
||||
}
|
||||
|
||||
func (i *inMemoryHelper) Add(credentials *credentials.Credentials) error {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
i.credentials[credentials.ServerURL] = *credentials
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *inMemoryHelper) Get(serverURL string) (string, string, error) {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
if result, ok := i.credentials[serverURL]; ok {
|
||||
return result.Username, result.Secret, nil
|
||||
}
|
||||
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
|
||||
func (i *inMemoryHelper) List() (map[string]string, error) {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
result := make(map[string]string, len(i.credentials))
|
||||
|
||||
for k, v := range i.credentials {
|
||||
result[k] = v.Username
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (i *inMemoryHelper) Delete(serverURL string) error {
|
||||
i.lock.Lock()
|
||||
defer i.lock.Unlock()
|
||||
|
||||
if _, ok := i.credentials[serverURL]; ok {
|
||||
delete(i.credentials, serverURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
return credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
390
docker/pusher.go
390
docker/pusher.go
|
|
@ -8,24 +8,22 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/pkg/docker/config"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
fn "knative.dev/kn-plugin-func"
|
||||
|
||||
containersTypes "github.com/containers/image/v5/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/daemon"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
type Opt func(*Pusher) error
|
||||
|
|
@ -37,235 +35,17 @@ type Credentials struct {
|
|||
|
||||
type CredentialsProvider func(ctx context.Context, registry string) (Credentials, error)
|
||||
|
||||
type CredentialsCallback func(registry string) (Credentials, error)
|
||||
|
||||
var ErrUnauthorized = errors.New("bad credentials")
|
||||
|
||||
// VerifyCredentialsCallback checks if credentials are accepted by the registry.
|
||||
// If credentials are incorrect this callback shall return ErrUnauthorized.
|
||||
type VerifyCredentialsCallback func(ctx context.Context, registry string, credentials Credentials) error
|
||||
|
||||
func CheckAuth(ctx context.Context, registry string, credentials Credentials) error {
|
||||
serverAddress := registry
|
||||
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
|
||||
serverAddress = "https://" + serverAddress
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v2", serverAddress)
|
||||
|
||||
authenticator := &authn.Basic{
|
||||
Username: credentials.Username,
|
||||
Password: credentials.Password,
|
||||
}
|
||||
|
||||
reg, err := name.NewRegistry(registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tr, err := transport.NewWithContext(ctx, reg, authenticator, http.DefaultTransport, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli := http.Client{Transport: tr}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := cli.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify credentials: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch {
|
||||
case resp.StatusCode == http.StatusUnauthorized:
|
||||
return ErrUnauthorized
|
||||
case resp.StatusCode != http.StatusOK:
|
||||
return fmt.Errorf("failed to verify credentials: status code: %d", resp.StatusCode)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
// PusherDockerClient is sub-interface of client.CommonAPIClient required by pusher.
|
||||
type PusherDockerClient interface {
|
||||
NegotiateAPIVersion(ctx context.Context)
|
||||
ImageSave(context.Context, []string) (io.ReadCloser, error)
|
||||
ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error)
|
||||
ImageTag(context.Context, string, string) error
|
||||
ImagePush(ctx context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type ChooseCredentialHelperCallback func(available []string) (string, error)
|
||||
|
||||
type credentialProviderConfig struct {
|
||||
promptForCredentials CredentialsCallback
|
||||
verifyCredentials VerifyCredentialsCallback
|
||||
promptForCredentialStore ChooseCredentialHelperCallback
|
||||
additionalCredentialLoaders []CredentialsCallback
|
||||
}
|
||||
|
||||
type CredentialProviderOptions func(opts *credentialProviderConfig)
|
||||
|
||||
// WithPromptForCredentials sets custom callback that is supposed to
|
||||
// interactively ask for credentials in case the credentials cannot be found in configuration files.
|
||||
// The callback may be called multiple times in case incorrect credentials were returned before.
|
||||
func WithPromptForCredentials(cbk CredentialsCallback) CredentialProviderOptions {
|
||||
return func(opts *credentialProviderConfig) {
|
||||
opts.promptForCredentials = cbk
|
||||
}
|
||||
}
|
||||
|
||||
// WithVerifyCredentials sets custom callback for credentials validation.
|
||||
func WithVerifyCredentials(cbk VerifyCredentialsCallback) CredentialProviderOptions {
|
||||
return func(opts *credentialProviderConfig) {
|
||||
opts.verifyCredentials = cbk
|
||||
}
|
||||
}
|
||||
|
||||
// WithPromptForCredentialStore sets custom callback that is supposed to
|
||||
// interactively ask user which credentials store/helper is used to store credentials obtained
|
||||
// from user.
|
||||
func WithPromptForCredentialStore(cbk ChooseCredentialHelperCallback) CredentialProviderOptions {
|
||||
return func(opts *credentialProviderConfig) {
|
||||
opts.promptForCredentialStore = cbk
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditionalCredentialLoaders adds custom callbacks for credential retrieval.
|
||||
// The callbacks are supposed to be non-interactive as opposed to WithPromptForCredentials.
|
||||
//
|
||||
// This might be useful when credentials are shared with some other service.
|
||||
//
|
||||
// Example: OpenShift builtin registry shares credentials with the cluster (k8s) credentials.
|
||||
func WithAdditionalCredentialLoaders(loaders ...CredentialsCallback) CredentialProviderOptions {
|
||||
return func(opts *credentialProviderConfig) {
|
||||
opts.additionalCredentialLoaders = append(opts.additionalCredentialLoaders, loaders...)
|
||||
}
|
||||
}
|
||||
|
||||
// NewCredentialsProvider returns new CredentialsProvider that tires to get credentials from docker/func config files.
|
||||
//
|
||||
// In case getting credentials from the config files fails
|
||||
// the caller provided callback (see WithPromptForCredentials) will be invoked to obtain credentials.
|
||||
// The callback may be called multiple times in case the returned credentials
|
||||
// are not correct (see WithVerifyCredentials).
|
||||
//
|
||||
// When the callback succeeds the credentials will be saved by using helper defined in the func config.
|
||||
// If the helper is not defined in the config file
|
||||
// it may be picked by provided callback (see WithPromptForCredentialStore).
|
||||
// The picked value will be saved in the func config.
|
||||
//
|
||||
// To verify that credentials are correct callback will be used (see WithVerifyCredentials).
|
||||
// If the callback is not set then CheckAuth will be used as a fallback.
|
||||
func NewCredentialsProvider(opts ...CredentialProviderOptions) CredentialsProvider {
|
||||
var conf credentialProviderConfig
|
||||
|
||||
for _, o := range opts {
|
||||
o(&conf)
|
||||
}
|
||||
|
||||
askUser, verifyCredentials, chooseCredentialHelper := conf.promptForCredentials, conf.verifyCredentials, conf.promptForCredentialStore
|
||||
|
||||
if verifyCredentials == nil {
|
||||
verifyCredentials = CheckAuth
|
||||
}
|
||||
|
||||
if chooseCredentialHelper == nil {
|
||||
chooseCredentialHelper = func(available []string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
authFilePath := filepath.Join(fn.ConfigPath(), "auth.json")
|
||||
sys := &containersTypes.SystemContext{
|
||||
AuthFilePath: authFilePath,
|
||||
}
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
//return result, fmt.Errorf("failed to determine home directory: %w", err)
|
||||
}
|
||||
dockerConfigPath := filepath.Join(home, ".docker", "config.json")
|
||||
|
||||
var authLoaders = []CredentialsCallback{
|
||||
func(registry string) (Credentials, error) {
|
||||
creds, err := config.GetCredentials(sys, registry)
|
||||
if err != nil {
|
||||
return Credentials{}, err
|
||||
}
|
||||
return Credentials{
|
||||
Username: creds.Username,
|
||||
Password: creds.Password,
|
||||
}, nil
|
||||
},
|
||||
func(registry string) (Credentials, error) {
|
||||
return getCredentialsByCredentialHelper(authFilePath, registry)
|
||||
},
|
||||
func(registry string) (Credentials, error) {
|
||||
return getCredentialsByCredentialHelper(dockerConfigPath, registry)
|
||||
},
|
||||
}
|
||||
|
||||
authLoaders = append(conf.additionalCredentialLoaders, authLoaders...)
|
||||
|
||||
return func(ctx context.Context, registry string) (Credentials, error) {
|
||||
result := Credentials{}
|
||||
|
||||
for _, load := range authLoaders {
|
||||
|
||||
result, err = load(registry)
|
||||
|
||||
if err != nil && !errors.Is(err, errCredentialsNotFound) {
|
||||
return Credentials{}, err
|
||||
}
|
||||
|
||||
if result != (Credentials{}) {
|
||||
err = verifyCredentials(ctx, registry, result)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
} else {
|
||||
if !errors.Is(err, ErrUnauthorized) {
|
||||
return Credentials{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
result, err = askUser(registry)
|
||||
if err != nil {
|
||||
return Credentials{}, err
|
||||
}
|
||||
|
||||
err = verifyCredentials(ctx, registry, result)
|
||||
if err == nil {
|
||||
err = setCredentialsByCredentialHelper(authFilePath, registry, result.Username, result.Password)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errNoCredentialHelperConfigured) {
|
||||
return Credentials{}, err
|
||||
}
|
||||
helpers := listCredentialHelpers()
|
||||
helper, err := chooseCredentialHelper(helpers)
|
||||
if err != nil {
|
||||
return Credentials{}, err
|
||||
}
|
||||
helper = strings.TrimPrefix(helper, "docker-credential-")
|
||||
err = setCredentialHelperToConfig(authFilePath, helper)
|
||||
if err != nil {
|
||||
return Credentials{}, fmt.Errorf("faild to set the helper to the config: %w", err)
|
||||
}
|
||||
err = setCredentialsByCredentialHelper(authFilePath, registry, result.Username, result.Password)
|
||||
if err != nil && !errors.Is(err, errNoCredentialHelperConfigured) {
|
||||
return Credentials{}, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
} else {
|
||||
if errors.Is(err, ErrUnauthorized) {
|
||||
continue
|
||||
}
|
||||
return Credentials{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
type PusherDockerClientFactory func() (PusherDockerClient, error)
|
||||
|
||||
// Pusher of images from local to remote registry.
|
||||
type Pusher struct {
|
||||
|
|
@ -273,6 +53,8 @@ type Pusher struct {
|
|||
Verbose bool
|
||||
credentialsProvider CredentialsProvider
|
||||
progressListener fn.ProgressListener
|
||||
transport http.RoundTripper
|
||||
dockerClientFactory PusherDockerClientFactory
|
||||
}
|
||||
|
||||
func WithCredentialsProvider(cp CredentialsProvider) Opt {
|
||||
|
|
@ -289,6 +71,20 @@ func WithProgressListener(pl fn.ProgressListener) Opt {
|
|||
}
|
||||
}
|
||||
|
||||
func WithTransport(transport http.RoundTripper) Opt {
|
||||
return func(pusher *Pusher) error {
|
||||
pusher.transport = transport
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithPusherDockerClientFactory(dockerClientFactory PusherDockerClientFactory) Opt {
|
||||
return func(pusher *Pusher) error {
|
||||
pusher.dockerClientFactory = dockerClientFactory
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func EmptyCredentialsProvider(ctx context.Context, registry string) (Credentials, error) {
|
||||
return Credentials{}, nil
|
||||
}
|
||||
|
|
@ -299,6 +95,11 @@ func NewPusher(opts ...Opt) (*Pusher, error) {
|
|||
Verbose: false,
|
||||
credentialsProvider: EmptyCredentialsProvider,
|
||||
progressListener: &fn.NoopProgressListener{},
|
||||
transport: http.DefaultTransport,
|
||||
dockerClientFactory: func() (PusherDockerClient, error) {
|
||||
c, _, err := NewClient(client.DefaultDockerHost)
|
||||
return c, err
|
||||
},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
err := opt(result)
|
||||
|
|
@ -306,10 +107,11 @@ func NewPusher(opts ...Opt) (*Pusher, error) {
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getRegistry(image_url string) (string, error) {
|
||||
func GetRegistry(image_url string) (string, error) {
|
||||
var registry string
|
||||
parts := strings.Split(image_url, "/")
|
||||
switch {
|
||||
|
|
@ -327,21 +129,23 @@ func getRegistry(image_url string) (string, error) {
|
|||
// Push the image of the Function.
|
||||
func (n *Pusher) Push(ctx context.Context, f fn.Function) (digest string, err error) {
|
||||
|
||||
var output io.Writer
|
||||
|
||||
if n.Verbose {
|
||||
output = os.Stderr
|
||||
} else {
|
||||
output = io.Discard
|
||||
}
|
||||
|
||||
if f.Image == "" {
|
||||
return "", errors.New("Function has no associated image. Has it been built?")
|
||||
}
|
||||
|
||||
registry, err := getRegistry(f.Image)
|
||||
registry, err := GetRegistry(f.Image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cli, _, err := NewClient(client.DefaultDockerHost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create docker api client: %w", err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
n.progressListener.Stopping()
|
||||
credentials, err := n.credentialsProvider(ctx, registry)
|
||||
if err != nil {
|
||||
|
|
@ -349,10 +153,25 @@ func (n *Pusher) Push(ctx context.Context, f fn.Function) (digest string, err er
|
|||
}
|
||||
n.progressListener.Increment("Pushing function image to the registry")
|
||||
|
||||
// if the registry is not cluster private do push directly from daemon
|
||||
if _, err = net.DefaultResolver.LookupHost(ctx, registry); err == nil {
|
||||
return n.daemonPush(ctx, f, credentials, output)
|
||||
}
|
||||
|
||||
// push with custom transport to be able to push into cluster private registries
|
||||
return n.push(ctx, f, credentials, output)
|
||||
}
|
||||
|
||||
func (n *Pusher) daemonPush(ctx context.Context, f fn.Function, credentials Credentials, output io.Writer) (digest string, err error) {
|
||||
cli, err := n.dockerClientFactory()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create docker api client: %w", err)
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
authConfig := types.AuthConfig{
|
||||
Username: credentials.Username,
|
||||
Password: credentials.Password,
|
||||
ServerAddress: registry,
|
||||
Username: credentials.Username,
|
||||
Password: credentials.Password,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&authConfig)
|
||||
|
|
@ -368,15 +187,8 @@ func (n *Pusher) Push(ctx context.Context, f fn.Function) (digest string, err er
|
|||
}
|
||||
defer r.Close()
|
||||
|
||||
var output io.Writer
|
||||
var outBuff bytes.Buffer
|
||||
|
||||
// If verbose logging is enabled, echo chatty stdout.
|
||||
if n.Verbose {
|
||||
output = io.MultiWriter(&outBuff, os.Stdout)
|
||||
} else {
|
||||
output = &outBuff
|
||||
}
|
||||
output = io.MultiWriter(&outBuff, output)
|
||||
|
||||
decoder := json.NewDecoder(r)
|
||||
li := logItem{}
|
||||
|
|
@ -403,17 +215,17 @@ func (n *Pusher) Push(ctx context.Context, f fn.Function) (digest string, err er
|
|||
fmt.Fprintf(output, "%s (%d%%)\n", li.Status, percent)
|
||||
}
|
||||
|
||||
digest = parseDigest(outBuff.String())
|
||||
digest = ParseDigest(outBuff.String())
|
||||
|
||||
return
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
var digestRE = regexp.MustCompile(`digest:\s+(sha256:\w{64})`)
|
||||
|
||||
// parseDigest tries to parse the last line from the output, which holds the pushed image digest
|
||||
// ParseDigest tries to parse the last line from the output, which holds the pushed image digest
|
||||
// The output should contain line like this:
|
||||
// latest: digest: sha256:a278a91112d17f8bde6b5f802a3317c7c752cf88078dae6f4b5a0784deb81782 size: 2613
|
||||
func parseDigest(output string) string {
|
||||
func ParseDigest(output string) string {
|
||||
match := digestRE.FindStringSubmatch(output)
|
||||
if len(match) >= 2 {
|
||||
return match[1]
|
||||
|
|
@ -438,3 +250,65 @@ type logItem struct {
|
|||
Progress string `json:"progress"`
|
||||
ProgressDetail progressDetail `json:"progressDetail"`
|
||||
}
|
||||
|
||||
func (n *Pusher) push(ctx context.Context, f fn.Function, credentials Credentials, output io.Writer) (digest string, err error) {
|
||||
auth := &authn.Basic{
|
||||
Username: credentials.Username,
|
||||
Password: credentials.Password,
|
||||
}
|
||||
|
||||
ref, err := name.ParseReference(f.Image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dockerClient, err := n.dockerClientFactory()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create docker api client: %w", err)
|
||||
}
|
||||
defer dockerClient.Close()
|
||||
|
||||
img, err := daemon.Image(ref,
|
||||
daemon.WithContext(ctx),
|
||||
daemon.WithClient(dockerClient))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
progressChannel := make(chan v1.Update, 1024)
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
defer fmt.Fprint(output, "\n")
|
||||
|
||||
for progress := range progressChannel {
|
||||
if progress.Error != nil {
|
||||
errChan <- progress.Error
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(output, "\rprogress: %d%%", progress.Complete*100/progress.Total)
|
||||
}
|
||||
|
||||
errChan <- nil
|
||||
}()
|
||||
|
||||
err = remote.Write(ref, img,
|
||||
remote.WithAuth(auth),
|
||||
remote.WithProgress(progressChannel),
|
||||
remote.WithTransport(n.transport),
|
||||
remote.WithJobs(1),
|
||||
remote.WithContext(ctx))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = <-errChan
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash, err := img.Digest()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hash.String(), nil
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
|
@ -22,14 +22,23 @@ type RoundTripCloser interface {
|
|||
//
|
||||
// This is useful for accessing cluster internal services (pushing a CloudEvent into Knative broker).
|
||||
func NewRoundTripper() RoundTripCloser {
|
||||
d := &dialer{
|
||||
netDialer: net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
return &roundTripCloser{
|
||||
Transport: http.Transport{
|
||||
result := &roundTripCloser{}
|
||||
if dt, ok := http.DefaultTransport.(*http.Transport); ok {
|
||||
d := &dialer{
|
||||
defaultDialContext: dt.DialContext,
|
||||
}
|
||||
result.d = d
|
||||
result.Transport = dt.Clone()
|
||||
result.Transport.DialContext = d.DialContext
|
||||
} else {
|
||||
d := &dialer{
|
||||
defaultDialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
}
|
||||
result.d = d
|
||||
result.Transport = &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: d.DialContext,
|
||||
ForceAttemptHTTP2: true,
|
||||
|
|
@ -37,13 +46,14 @@ func NewRoundTripper() RoundTripCloser {
|
|||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
d: d,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type roundTripCloser struct {
|
||||
http.Transport
|
||||
*http.Transport
|
||||
d *dialer
|
||||
}
|
||||
|
||||
|
|
@ -52,13 +62,13 @@ func (r *roundTripCloser) Close() error {
|
|||
}
|
||||
|
||||
type dialer struct {
|
||||
o sync.Once
|
||||
netDialer net.Dialer
|
||||
inClusterDialer k8s.ContextDialer
|
||||
o sync.Once
|
||||
defaultDialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
inClusterDialer k8s.ContextDialer
|
||||
}
|
||||
|
||||
func (d *dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
conn, err := d.netDialer.DialContext(ctx, network, address)
|
||||
conn, err := d.defaultDialContext(ctx, network, address)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
|
|
|
|||
104
vendor/github.com/google/go-containerregistry/internal/httptest/httptest.go
generated
vendored
Normal file
104
vendor/github.com/google/go-containerregistry/internal/httptest/httptest.go
generated
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright 2020 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 httptest provides a method for testing a TLS server a la net/http/httptest.
|
||||
package httptest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewTLSServer returns an httptest server, with an http client that has been configured to
|
||||
// send all requests to the returned server. The TLS certs are generated for the given domain.
|
||||
// If you need a transport, Client().Transport is correctly configured.
|
||||
func NewTLSServer(domain string, handler http.Handler) (*httptest.Server, error) {
|
||||
s := httptest.NewUnstartedServer(handler)
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
NotBefore: time.Now().Add(-1 * time.Hour),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
IPAddresses: []net.IP{
|
||||
net.IPv4(127, 0, 0, 1),
|
||||
net.IPv6loopback,
|
||||
},
|
||||
DNSNames: []string{domain},
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc := &bytes.Buffer{}
|
||||
if err := pem.Encode(pc, &pem.Block{Type: "CERTIFICATE", Bytes: b}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ek, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pk := &bytes.Buffer{}
|
||||
if err := pem.Encode(pk, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ek}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := tls.X509KeyPair(pc.Bytes(), pk.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{c},
|
||||
}
|
||||
s.StartTLS()
|
||||
|
||||
certpool := x509.NewCertPool()
|
||||
certpool.AddCert(s.Certificate())
|
||||
|
||||
t := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: certpool,
|
||||
},
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return net.Dial(s.Listener.Addr().Network(), s.Listener.Addr().String())
|
||||
},
|
||||
}
|
||||
s.Client().Transport = t
|
||||
|
||||
return s, nil
|
||||
}
|
||||
224
vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go
generated
vendored
Normal file
224
vendor/github.com/google/go-containerregistry/pkg/registry/blobs.go
generated
vendored
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Returns whether this url should be handled by the blob handler
|
||||
// This is complicated because blob is indicated by the trailing path, not the leading path.
|
||||
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-a-layer
|
||||
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-a-layer
|
||||
func isBlob(req *http.Request) bool {
|
||||
elem := strings.Split(req.URL.Path, "/")
|
||||
elem = elem[1:]
|
||||
if elem[len(elem)-1] == "" {
|
||||
elem = elem[:len(elem)-1]
|
||||
}
|
||||
if len(elem) < 3 {
|
||||
return false
|
||||
}
|
||||
return elem[len(elem)-2] == "blobs" || (elem[len(elem)-3] == "blobs" &&
|
||||
elem[len(elem)-2] == "uploads")
|
||||
}
|
||||
|
||||
// blobs
|
||||
type blobs struct {
|
||||
// Blobs are content addresses. we store them globally underneath their sha and make no distinctions per image.
|
||||
contents map[string][]byte
|
||||
// Each upload gets a unique id that writes occur to until finalized.
|
||||
uploads map[string][]byte
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError {
|
||||
elem := strings.Split(req.URL.Path, "/")
|
||||
elem = elem[1:]
|
||||
if elem[len(elem)-1] == "" {
|
||||
elem = elem[:len(elem)-1]
|
||||
}
|
||||
// Must have a path of form /v2/{name}/blobs/{upload,sha256:}
|
||||
if len(elem) < 4 {
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "NAME_INVALID",
|
||||
Message: "blobs must be attached to a repo",
|
||||
}
|
||||
}
|
||||
target := elem[len(elem)-1]
|
||||
service := elem[len(elem)-2]
|
||||
digest := req.URL.Query().Get("digest")
|
||||
contentRange := req.Header.Get("Content-Range")
|
||||
|
||||
if req.Method == "HEAD" {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
b, ok := b.contents[target]
|
||||
if !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "BLOB_UNKNOWN",
|
||||
Message: "Unknown blob",
|
||||
}
|
||||
}
|
||||
|
||||
resp.Header().Set("Content-Length", fmt.Sprint(len(b)))
|
||||
resp.Header().Set("Docker-Content-Digest", target)
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "GET" {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
b, ok := b.contents[target]
|
||||
if !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "BLOB_UNKNOWN",
|
||||
Message: "Unknown blob",
|
||||
}
|
||||
}
|
||||
|
||||
resp.Header().Set("Content-Length", fmt.Sprint(len(b)))
|
||||
resp.Header().Set("Docker-Content-Digest", target)
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
io.Copy(resp, bytes.NewReader(b))
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "POST" && target == "uploads" && digest != "" {
|
||||
l := &bytes.Buffer{}
|
||||
io.Copy(l, req.Body)
|
||||
rd := sha256.Sum256(l.Bytes())
|
||||
d := "sha256:" + hex.EncodeToString(rd[:])
|
||||
if d != digest {
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "DIGEST_INVALID",
|
||||
Message: "digest does not match contents",
|
||||
}
|
||||
}
|
||||
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
b.contents[d] = l.Bytes()
|
||||
resp.Header().Set("Docker-Content-Digest", d)
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "POST" && target == "uploads" && digest == "" {
|
||||
id := fmt.Sprint(rand.Int63())
|
||||
resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id))
|
||||
resp.Header().Set("Range", "0-0")
|
||||
resp.WriteHeader(http.StatusAccepted)
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "PATCH" && service == "uploads" && contentRange != "" {
|
||||
start, end := 0, 0
|
||||
if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil {
|
||||
return ®Error{
|
||||
Status: http.StatusRequestedRangeNotSatisfiable,
|
||||
Code: "BLOB_UPLOAD_UNKNOWN",
|
||||
Message: "We don't understand your Content-Range",
|
||||
}
|
||||
}
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
if start != len(b.uploads[target]) {
|
||||
return ®Error{
|
||||
Status: http.StatusRequestedRangeNotSatisfiable,
|
||||
Code: "BLOB_UPLOAD_UNKNOWN",
|
||||
Message: "Your content range doesn't match what we have",
|
||||
}
|
||||
}
|
||||
l := bytes.NewBuffer(b.uploads[target])
|
||||
io.Copy(l, req.Body)
|
||||
b.uploads[target] = l.Bytes()
|
||||
resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target))
|
||||
resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1))
|
||||
resp.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "PATCH" && service == "uploads" && contentRange == "" {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
if _, ok := b.uploads[target]; ok {
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "BLOB_UPLOAD_INVALID",
|
||||
Message: "Stream uploads after first write are not allowed",
|
||||
}
|
||||
}
|
||||
|
||||
l := &bytes.Buffer{}
|
||||
io.Copy(l, req.Body)
|
||||
|
||||
b.uploads[target] = l.Bytes()
|
||||
resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target))
|
||||
resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1))
|
||||
resp.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "PUT" && service == "uploads" && digest == "" {
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "DIGEST_INVALID",
|
||||
Message: "digest not specified",
|
||||
}
|
||||
}
|
||||
|
||||
if req.Method == "PUT" && service == "uploads" && digest != "" {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
l := bytes.NewBuffer(b.uploads[target])
|
||||
io.Copy(l, req.Body)
|
||||
rd := sha256.Sum256(l.Bytes())
|
||||
d := "sha256:" + hex.EncodeToString(rd[:])
|
||||
if d != digest {
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "DIGEST_INVALID",
|
||||
Message: "digest does not match contents",
|
||||
}
|
||||
}
|
||||
|
||||
b.contents[d] = l.Bytes()
|
||||
delete(b.uploads, target)
|
||||
resp.Header().Set("Docker-Content-Digest", d)
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
return nil
|
||||
}
|
||||
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "METHOD_UNKNOWN",
|
||||
Message: "We don't understand your method + url",
|
||||
}
|
||||
}
|
||||
46
vendor/github.com/google/go-containerregistry/pkg/registry/error.go
generated
vendored
Normal file
46
vendor/github.com/google/go-containerregistry/pkg/registry/error.go
generated
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type regError struct {
|
||||
Status int
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (r *regError) Write(resp http.ResponseWriter) error {
|
||||
resp.WriteHeader(r.Status)
|
||||
|
||||
type err struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
type wrap struct {
|
||||
Errors []err `json:"errors"`
|
||||
}
|
||||
return json.NewEncoder(resp).Encode(wrap{
|
||||
Errors: []err{
|
||||
{
|
||||
Code: r.Code,
|
||||
Message: r.Message,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
334
vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go
generated
vendored
Normal file
334
vendor/github.com/google/go-containerregistry/pkg/registry/manifest.go
generated
vendored
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
)
|
||||
|
||||
type catalog struct {
|
||||
Repos []string `json:"repositories"`
|
||||
}
|
||||
|
||||
type listTags struct {
|
||||
Name string `json:"name"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type manifest struct {
|
||||
contentType string
|
||||
blob []byte
|
||||
}
|
||||
|
||||
type manifests struct {
|
||||
// maps repo -> manifest tag/digest -> manifest
|
||||
manifests map[string]map[string]manifest
|
||||
lock sync.Mutex
|
||||
log *log.Logger
|
||||
}
|
||||
|
||||
func isManifest(req *http.Request) bool {
|
||||
elems := strings.Split(req.URL.Path, "/")
|
||||
elems = elems[1:]
|
||||
if len(elems) < 4 {
|
||||
return false
|
||||
}
|
||||
return elems[len(elems)-2] == "manifests"
|
||||
}
|
||||
|
||||
func isTags(req *http.Request) bool {
|
||||
elems := strings.Split(req.URL.Path, "/")
|
||||
elems = elems[1:]
|
||||
if len(elems) < 4 {
|
||||
return false
|
||||
}
|
||||
return elems[len(elems)-2] == "tags"
|
||||
}
|
||||
|
||||
func isCatalog(req *http.Request) bool {
|
||||
elems := strings.Split(req.URL.Path, "/")
|
||||
elems = elems[1:]
|
||||
if len(elems) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return elems[len(elems)-1] == "_catalog"
|
||||
}
|
||||
|
||||
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest
|
||||
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image
|
||||
func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError {
|
||||
elem := strings.Split(req.URL.Path, "/")
|
||||
elem = elem[1:]
|
||||
target := elem[len(elem)-1]
|
||||
repo := strings.Join(elem[1:len(elem)-2], "/")
|
||||
|
||||
if req.Method == "GET" {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
c, ok := m.manifests[repo]
|
||||
if !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "NAME_UNKNOWN",
|
||||
Message: "Unknown name",
|
||||
}
|
||||
}
|
||||
m, ok := c[target]
|
||||
if !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "MANIFEST_UNKNOWN",
|
||||
Message: "Unknown manifest",
|
||||
}
|
||||
}
|
||||
rd := sha256.Sum256(m.blob)
|
||||
d := "sha256:" + hex.EncodeToString(rd[:])
|
||||
resp.Header().Set("Docker-Content-Digest", d)
|
||||
resp.Header().Set("Content-Type", m.contentType)
|
||||
resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
io.Copy(resp, bytes.NewReader(m.blob))
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "HEAD" {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
if _, ok := m.manifests[repo]; !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "NAME_UNKNOWN",
|
||||
Message: "Unknown name",
|
||||
}
|
||||
}
|
||||
m, ok := m.manifests[repo][target]
|
||||
if !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "MANIFEST_UNKNOWN",
|
||||
Message: "Unknown manifest",
|
||||
}
|
||||
}
|
||||
rd := sha256.Sum256(m.blob)
|
||||
d := "sha256:" + hex.EncodeToString(rd[:])
|
||||
resp.Header().Set("Docker-Content-Digest", d)
|
||||
resp.Header().Set("Content-Type", m.contentType)
|
||||
resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob)))
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "PUT" {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
if _, ok := m.manifests[repo]; !ok {
|
||||
m.manifests[repo] = map[string]manifest{}
|
||||
}
|
||||
b := &bytes.Buffer{}
|
||||
io.Copy(b, req.Body)
|
||||
rd := sha256.Sum256(b.Bytes())
|
||||
digest := "sha256:" + hex.EncodeToString(rd[:])
|
||||
mf := manifest{
|
||||
blob: b.Bytes(),
|
||||
contentType: req.Header.Get("Content-Type"),
|
||||
}
|
||||
|
||||
// If the manifest is a manifest list, check that the manifest
|
||||
// list's constituent manifests are already uploaded.
|
||||
// This isn't strictly required by the registry API, but some
|
||||
// registries require this.
|
||||
if types.MediaType(mf.contentType).IsIndex() {
|
||||
im, err := v1.ParseIndexManifest(b)
|
||||
if err != nil {
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "MANIFEST_INVALID",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
for _, desc := range im.Manifests {
|
||||
if !desc.MediaType.IsDistributable() {
|
||||
continue
|
||||
}
|
||||
if desc.MediaType.IsIndex() || desc.MediaType.IsImage() {
|
||||
if _, found := m.manifests[repo][desc.Digest.String()]; !found {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "MANIFEST_UNKNOWN",
|
||||
Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: Probably want to do an existence check for blobs.
|
||||
m.log.Printf("TODO: Check blobs for %q", desc.Digest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow future references by target (tag) and immutable digest.
|
||||
// See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier.
|
||||
m.manifests[repo][target] = mf
|
||||
m.manifests[repo][digest] = mf
|
||||
resp.Header().Set("Docker-Content-Digest", digest)
|
||||
resp.WriteHeader(http.StatusCreated)
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.Method == "DELETE" {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
if _, ok := m.manifests[repo]; !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "NAME_UNKNOWN",
|
||||
Message: "Unknown name",
|
||||
}
|
||||
}
|
||||
|
||||
_, ok := m.manifests[repo][target]
|
||||
if !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "MANIFEST_UNKNOWN",
|
||||
Message: "Unknown manifest",
|
||||
}
|
||||
}
|
||||
|
||||
delete(m.manifests[repo], target)
|
||||
resp.WriteHeader(http.StatusAccepted)
|
||||
return nil
|
||||
}
|
||||
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "METHOD_UNKNOWN",
|
||||
Message: "We don't understand your method + url",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *regError {
|
||||
elem := strings.Split(req.URL.Path, "/")
|
||||
elem = elem[1:]
|
||||
repo := strings.Join(elem[1:len(elem)-2], "/")
|
||||
query := req.URL.Query()
|
||||
nStr := query.Get("n")
|
||||
n := 1000
|
||||
if nStr != "" {
|
||||
n, _ = strconv.Atoi(nStr)
|
||||
}
|
||||
|
||||
if req.Method == "GET" {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
c, ok := m.manifests[repo]
|
||||
if !ok {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "NAME_UNKNOWN",
|
||||
Message: "Unknown name",
|
||||
}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
countTags := 0
|
||||
// TODO: implement pagination https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated
|
||||
for tag := range c {
|
||||
if countTags >= n {
|
||||
break
|
||||
}
|
||||
countTags++
|
||||
if !strings.Contains(tag, "sha256:") {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
sort.Strings(tags)
|
||||
|
||||
tagsToList := listTags{
|
||||
Name: repo,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
msg, _ := json.Marshal(tagsToList)
|
||||
resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
io.Copy(resp, bytes.NewReader([]byte(msg)))
|
||||
return nil
|
||||
}
|
||||
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "METHOD_UNKNOWN",
|
||||
Message: "We don't understand your method + url",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError {
|
||||
query := req.URL.Query()
|
||||
nStr := query.Get("n")
|
||||
n := 10000
|
||||
if nStr != "" {
|
||||
n, _ = strconv.Atoi(nStr)
|
||||
}
|
||||
|
||||
if req.Method == "GET" {
|
||||
m.lock.Lock()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
var repos []string
|
||||
countRepos := 0
|
||||
// TODO: implement pagination
|
||||
for key := range m.manifests {
|
||||
if countRepos >= n {
|
||||
break
|
||||
}
|
||||
countRepos++
|
||||
|
||||
repos = append(repos, key)
|
||||
}
|
||||
|
||||
repositoriesToList := catalog{
|
||||
Repos: repos,
|
||||
}
|
||||
|
||||
msg, _ := json.Marshal(repositoriesToList)
|
||||
resp.Header().Set("Content-Length", fmt.Sprint(len(msg)))
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
io.Copy(resp, bytes.NewReader([]byte(msg)))
|
||||
return nil
|
||||
}
|
||||
|
||||
return ®Error{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: "METHOD_UNKNOWN",
|
||||
Message: "We don't understand your method + url",
|
||||
}
|
||||
}
|
||||
104
vendor/github.com/google/go-containerregistry/pkg/registry/registry.go
generated
vendored
Normal file
104
vendor/github.com/google/go-containerregistry/pkg/registry/registry.go
generated
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 registry implements a docker V2 registry and the OCI distribution specification.
|
||||
//
|
||||
// It is designed to be used anywhere a low dependency container registry is needed, with an
|
||||
// initial focus on tests.
|
||||
//
|
||||
// Its goal is to be standards compliant and its strictness will increase over time.
|
||||
//
|
||||
// This is currently a low flightmiles system. It's likely quite safe to use in tests; If you're using it
|
||||
// in production, please let us know how and send us CL's for integration tests.
|
||||
package registry
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type registry struct {
|
||||
log *log.Logger
|
||||
blobs blobs
|
||||
manifests manifests
|
||||
}
|
||||
|
||||
// https://docs.docker.com/registry/spec/api/#api-version-check
|
||||
// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#api-version-check
|
||||
func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError {
|
||||
if isBlob(req) {
|
||||
return r.blobs.handle(resp, req)
|
||||
}
|
||||
if isManifest(req) {
|
||||
return r.manifests.handle(resp, req)
|
||||
}
|
||||
if isTags(req) {
|
||||
return r.manifests.handleTags(resp, req)
|
||||
}
|
||||
if isCatalog(req) {
|
||||
return r.manifests.handleCatalog(resp, req)
|
||||
}
|
||||
resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
|
||||
if req.URL.Path != "/v2/" && req.URL.Path != "/v2" {
|
||||
return ®Error{
|
||||
Status: http.StatusNotFound,
|
||||
Code: "METHOD_UNKNOWN",
|
||||
Message: "We don't understand your method + url",
|
||||
}
|
||||
}
|
||||
resp.WriteHeader(200)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *registry) root(resp http.ResponseWriter, req *http.Request) {
|
||||
if rerr := r.v2(resp, req); rerr != nil {
|
||||
r.log.Printf("%s %s %d %s %s", req.Method, req.URL, rerr.Status, rerr.Code, rerr.Message)
|
||||
rerr.Write(resp)
|
||||
return
|
||||
}
|
||||
r.log.Printf("%s %s", req.Method, req.URL)
|
||||
}
|
||||
|
||||
// New returns a handler which implements the docker registry protocol.
|
||||
// It should be registered at the site root.
|
||||
func New(opts ...Option) http.Handler {
|
||||
r := ®istry{
|
||||
log: log.New(os.Stderr, "", log.LstdFlags),
|
||||
blobs: blobs{
|
||||
contents: map[string][]byte{},
|
||||
uploads: map[string][]byte{},
|
||||
},
|
||||
manifests: manifests{
|
||||
manifests: map[string]map[string]manifest{},
|
||||
log: log.New(os.Stderr, "", log.LstdFlags),
|
||||
},
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(r)
|
||||
}
|
||||
return http.HandlerFunc(r.root)
|
||||
}
|
||||
|
||||
// Option describes the available options
|
||||
// for creating the registry.
|
||||
type Option func(r *registry)
|
||||
|
||||
// Logger overrides the logger used to record requests to the registry.
|
||||
func Logger(l *log.Logger) Option {
|
||||
return func(r *registry) {
|
||||
r.log = l
|
||||
r.manifests.log = l
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 registry
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
|
||||
ggcrtest "github.com/google/go-containerregistry/internal/httptest"
|
||||
)
|
||||
|
||||
// TLS returns an httptest server, with an http client that has been configured to
|
||||
// send all requests to the returned server. The TLS certs are generated for the given domain
|
||||
// which should correspond to the domain the image is stored in.
|
||||
// If you need a transport, Client().Transport is correctly configured.
|
||||
func TLS(domain string) (*httptest.Server, error) {
|
||||
return ggcrtest.NewTLSServer(domain, New())
|
||||
}
|
||||
11
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/README.md
generated
vendored
Normal file
11
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# `daemon`
|
||||
|
||||
[](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon)
|
||||
|
||||
The `daemon` package enables reading/writing images from/to the docker daemon.
|
||||
|
||||
It is not fully fleshed out, but is useful for interoperability, see various issues:
|
||||
|
||||
* https://github.com/google/go-containerregistry/issues/205
|
||||
* https://github.com/google/go-containerregistry/issues/552
|
||||
* https://github.com/google/go-containerregistry/issues/627
|
||||
17
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/doc.go
generated
vendored
Normal file
17
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 daemon provides facilities for reading/writing v1.Image from/to
|
||||
// a running daemon.
|
||||
package daemon
|
||||
89
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go
generated
vendored
Normal file
89
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/image.go
generated
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
)
|
||||
|
||||
type imageOpener struct {
|
||||
ref name.Reference
|
||||
ctx context.Context
|
||||
|
||||
buffered bool
|
||||
client Client
|
||||
|
||||
once sync.Once
|
||||
bytes []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (i *imageOpener) saveImage() (io.ReadCloser, error) {
|
||||
return i.client.ImageSave(i.ctx, []string{i.ref.Name()})
|
||||
}
|
||||
|
||||
func (i *imageOpener) bufferedOpener() (io.ReadCloser, error) {
|
||||
// Store the tarball in memory and return a new reader into the bytes each time we need to access something.
|
||||
i.once.Do(func() {
|
||||
i.bytes, i.err = func() ([]byte, error) {
|
||||
rc, err := i.saveImage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
return ioutil.ReadAll(rc)
|
||||
}()
|
||||
})
|
||||
|
||||
// Wrap the bytes in a ReadCloser so it looks like an opened file.
|
||||
return ioutil.NopCloser(bytes.NewReader(i.bytes)), i.err
|
||||
}
|
||||
|
||||
func (i *imageOpener) opener() tarball.Opener {
|
||||
if i.buffered {
|
||||
return i.bufferedOpener
|
||||
}
|
||||
|
||||
// To avoid storing the tarball in memory, do a save every time we need to access something.
|
||||
return i.saveImage
|
||||
}
|
||||
|
||||
// Image provides access to an image reference from the Docker daemon,
|
||||
// applying functional options to the underlying imageOpener before
|
||||
// resolving the reference into a v1.Image.
|
||||
func Image(ref name.Reference, options ...Option) (v1.Image, error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i := &imageOpener{
|
||||
ref: ref,
|
||||
buffered: o.buffered,
|
||||
client: o.client,
|
||||
ctx: o.ctx,
|
||||
}
|
||||
|
||||
return tarball.Image(i.opener(), nil)
|
||||
}
|
||||
102
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go
generated
vendored
Normal file
102
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/options.go
generated
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
// ImageOption is an alias for Option.
|
||||
// Deprecated: Use Option instead.
|
||||
type ImageOption Option
|
||||
|
||||
// Option is a functional option for daemon operations.
|
||||
type Option func(*options)
|
||||
|
||||
type options struct {
|
||||
ctx context.Context
|
||||
client Client
|
||||
buffered bool
|
||||
}
|
||||
|
||||
var defaultClient = func() (Client, error) {
|
||||
return client.NewClientWithOpts(client.FromEnv)
|
||||
}
|
||||
|
||||
func makeOptions(opts ...Option) (*options, error) {
|
||||
o := &options{
|
||||
buffered: true,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(o)
|
||||
}
|
||||
|
||||
if o.client == nil {
|
||||
client, err := defaultClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.client = client
|
||||
}
|
||||
o.client.NegotiateAPIVersion(o.ctx)
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// WithBufferedOpener buffers the image.
|
||||
func WithBufferedOpener() Option {
|
||||
return func(o *options) {
|
||||
o.buffered = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithUnbufferedOpener streams the image to avoid buffering.
|
||||
func WithUnbufferedOpener() Option {
|
||||
return func(o *options) {
|
||||
o.buffered = false
|
||||
}
|
||||
}
|
||||
|
||||
// WithClient is a functional option to allow injecting a docker client.
|
||||
//
|
||||
// By default, github.com/docker/docker/client.FromEnv is used.
|
||||
func WithClient(client Client) Option {
|
||||
return func(o *options) {
|
||||
o.client = client
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext is a functional option to pass through a context.Context.
|
||||
//
|
||||
// By default, context.Background() is used.
|
||||
func WithContext(ctx context.Context) Option {
|
||||
return func(o *options) {
|
||||
o.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// Client represents the subset of a docker client that the daemon
|
||||
// package uses.
|
||||
type Client interface {
|
||||
NegotiateAPIVersion(ctx context.Context)
|
||||
ImageSave(context.Context, []string) (io.ReadCloser, error)
|
||||
ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error)
|
||||
ImageTag(context.Context, string, string) error
|
||||
}
|
||||
61
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go
generated
vendored
Normal file
61
vendor/github.com/google/go-containerregistry/pkg/v1/daemon/write.go
generated
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2018 Google LLC All Rights Reserved.
|
||||
//
|
||||
// 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 daemon
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
)
|
||||
|
||||
// Tag adds a tag to an already existent image.
|
||||
func Tag(src, dest name.Tag, options ...Option) error {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return o.client.ImageTag(o.ctx, src.String(), dest.String())
|
||||
}
|
||||
|
||||
// Write saves the image into the daemon as the given tag.
|
||||
func Write(tag name.Tag, img v1.Image, options ...Option) (string, error) {
|
||||
o, err := makeOptions(options...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
pw.CloseWithError(tarball.Write(tag, img, pw))
|
||||
}()
|
||||
|
||||
// write the image in docker save format first, then load it
|
||||
resp, err := o.client.ImageLoad(o.ctx, pr, false)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error loading image: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
response := string(b)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("error reading load response body: %v", err)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -400,6 +400,7 @@ github.com/google/go-cmp/cmp/internal/value
|
|||
github.com/google/go-containerregistry/internal/and
|
||||
github.com/google/go-containerregistry/internal/estargz
|
||||
github.com/google/go-containerregistry/internal/gzip
|
||||
github.com/google/go-containerregistry/internal/httptest
|
||||
github.com/google/go-containerregistry/internal/redact
|
||||
github.com/google/go-containerregistry/internal/retry
|
||||
github.com/google/go-containerregistry/internal/retry/wait
|
||||
|
|
@ -407,7 +408,9 @@ github.com/google/go-containerregistry/internal/verify
|
|||
github.com/google/go-containerregistry/pkg/authn
|
||||
github.com/google/go-containerregistry/pkg/logs
|
||||
github.com/google/go-containerregistry/pkg/name
|
||||
github.com/google/go-containerregistry/pkg/registry
|
||||
github.com/google/go-containerregistry/pkg/v1
|
||||
github.com/google/go-containerregistry/pkg/v1/daemon
|
||||
github.com/google/go-containerregistry/pkg/v1/empty
|
||||
github.com/google/go-containerregistry/pkg/v1/layout
|
||||
github.com/google/go-containerregistry/pkg/v1/match
|
||||
|
|
|
|||
Loading…
Reference in New Issue