From a12a396aabbd9fb2e67e165de44c00768c1769a0 Mon Sep 17 00:00:00 2001 From: Josh Dolitsky Date: Mon, 6 May 2019 16:15:34 -0500 Subject: [PATCH] Helm 3: registry login/logout (#5597) * login/logout placeholders Signed-off-by: Josh Dolitsky * use latest oras Signed-off-by: Josh Dolitsky * use docker auth system Signed-off-by: Josh Dolitsky * working login+push Signed-off-by: Josh Dolitsky * working on tests Signed-off-by: Josh Dolitsky * fix typo in htpasswd Signed-off-by: Josh Dolitsky * rename credsfile to config.json Signed-off-by: Josh Dolitsky * add flags for username/password Signed-off-by: Josh Dolitsky * disable logout test broken on linux Signed-off-by: Josh Dolitsky * upgrade to oras 0.4.0 Signed-off-by: Josh Dolitsky * re-enable logout test Signed-off-by: Josh Dolitsky * panic for uncaught errors Signed-off-by: Josh Dolitsky * move login/logout to new registry subcommand Signed-off-by: Josh Dolitsky --- Gopkg.lock | 124 ++++++++++++++++++++++++++++--- Gopkg.toml | 7 +- cmd/helm/chart.go | 8 +- cmd/helm/registry.go | 45 ++++++++++++ cmd/helm/registry_login.go | 134 ++++++++++++++++++++++++++++++++++ cmd/helm/registry_logout.go | 43 +++++++++++ cmd/helm/root.go | 22 +++++- pkg/action/registry_login.go | 38 ++++++++++ pkg/action/registry_logout.go | 38 ++++++++++ pkg/engine/funcs.go | 2 +- pkg/registry/authorizer.go | 28 +++++++ pkg/registry/cache.go | 2 +- pkg/registry/client.go | 41 +++++++++-- pkg/registry/client_test.go | 73 ++++++++++++++---- 14 files changed, 557 insertions(+), 48 deletions(-) create mode 100644 cmd/helm/registry.go create mode 100644 cmd/helm/registry_login.go create mode 100644 cmd/helm/registry_logout.go create mode 100644 pkg/action/registry_login.go create mode 100644 pkg/action/registry_logout.go create mode 100644 pkg/registry/authorizer.go diff --git a/Gopkg.lock b/Gopkg.lock index 7e4514968..4edbe1b21 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -91,6 +91,22 @@ revision = "b4f55832432b95a611cf1495272b5c8e24952a1a" version = "v1.13.0" +[[projects]] + digest = "1:f9ae348e1f793dcf9ed930ed47136a67343dbd6809c5c91391322267f4476892" + name = "github.com/Microsoft/go-winio" + packages = ["."] + pruneopts = "UT" + revision = "1a8911d1ed007260465c3bfbbc785ac6915a0bb8" + version = "v0.4.12" + +[[projects]] + branch = "master" + digest = "1:3721a10686511b80c052323423f0de17a8c06d417dbdd3b392b1578432a33aae" + name = "github.com/Nvveen/Gotty" + packages = ["."] + pruneopts = "UT" + revision = "cd527374f1e5bff4938207604a14f2e38a9cf512" + [[projects]] digest = "1:d1665c44bd5db19aaee18d1b6233c99b0b9a986e8bccb24ef54747547a48027f" name = "github.com/PuerkitoBio/purell" @@ -185,7 +201,7 @@ revision = "bf70f2a70fb1b1f36d90d671a72795984eab0fcb" [[projects]] - digest = "1:1872596d45cb52913fcdea90d468f3e57435959e9c0d99ccb316ee76de341313" + digest = "1:37f8940c4d3c41536ea882b1ca3498e403c04892dfc34bd0d670ed9eafccda9a" name = "github.com/containerd/containerd" packages = [ "content", @@ -198,8 +214,16 @@ "remotes/docker", ] pruneopts = "UT" - revision = "9b32062dc1f5a7c2564315c269b5059754f12b9d" - version = "v1.2.1" + revision = "894b81a4b802e4eb2a91d1ce216b8817763c29fb" + version = "v1.2.6" + +[[projects]] + branch = "master" + digest = "1:e48c63e818c67fbf3d7afe20bba33134ab1a5bf384847385384fd027652a5a96" + name = "github.com/containerd/continuity" + packages = ["pathdriver"] + pruneopts = "UT" + revision = "004b46473808b3e7a4a3049c20e4376c91eb966d" [[projects]] digest = "1:7cb4fdca4c251b3ef8027c90ea35f70c7b661a593b9eeae34753c65499098bb1" @@ -218,15 +242,17 @@ version = "v1.1.1" [[projects]] - digest = "1:8285cd51b86f5c3af447ad1db7c8572578a422cf9a50f1f07eea1d021151044f" + digest = "1:82158435e282da9b23bb1188487fe1c68b17a54ed9dcd557ab6204782ad3ff92" name = "github.com/deislabs/oras" packages = [ + "pkg/auth", + "pkg/auth/docker", "pkg/content", "pkg/oras", ] pruneopts = "UT" - revision = "e8a1fa6ff9a507b99eedd45745959e8c5b826d9f" - version = "v0.3.3" + revision = "9f7669048990b0d0c186985737e6a6c3bb3f7ecc" + version = "v0.4.0" [[projects]] digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55" @@ -237,7 +263,20 @@ version = "v3.2.0" [[projects]] - digest = "1:888aaacf886021e4a0fa6b09a61f1158063bd6c2e2ddefe14f3a7ccbc93ffe27" + digest = "1:f65090e4f60dcd4d2de69e8ebca022d59a8c6463a3a4c122e64cec91a83749ff" + name = "github.com/docker/cli" + packages = [ + "cli/config", + "cli/config/configfile", + "cli/config/credentials", + "opts", + ] + pruneopts = "UT" + revision = "c89750f836c57ce10386e71669e1b08a54c3caeb" + version = "v18.09.5" + +[[projects]] + digest = "1:feaf11ab67fe48ec2712bf9d44e2fb2d4ebdc5da8e5a47bd3ce05bae9f82825b" name = "github.com/docker/distribution" packages = [ ".", @@ -258,6 +297,7 @@ "registry/api/errcode", "registry/api/v2", "registry/auth", + "registry/auth/htpasswd", "registry/client", "registry/client/auth", "registry/client/auth/challenge", @@ -286,15 +326,61 @@ [[projects]] branch = "master" - digest = "1:8da8bb2b12c31c632e96ca6f15666a36c36cd390326b6c5e1c5e309cf4b5419a" + digest = "1:b5be0d9940d8fa3ff7df4949a8e8c47a7f93ea8251239ad074e1a6b0db55876a" name = "github.com/docker/docker" packages = [ + "api/types", + "api/types/blkiodev", + "api/types/container", + "api/types/filters", + "api/types/mount", + "api/types/network", + "api/types/registry", + "api/types/strslice", + "api/types/swarm", + "api/types/swarm/runtime", + "api/types/versions", + "errdefs", + "pkg/homedir", + "pkg/idtools", + "pkg/ioutils", + "pkg/jsonmessage", + "pkg/longpath", + "pkg/mount", + "pkg/stringid", + "pkg/system", + "pkg/tarsum", "pkg/term", "pkg/term/windows", + "registry", + "registry/resumable", ] pruneopts = "UT" revision = "2cb26cfe9cbf8a64c5046c74d65f4528b22e67f4" +[[projects]] + digest = "1:8866486038791fe65ea1abf660041423954b1f3fb99ea6a0ad8424422e943458" + name = "github.com/docker/docker-credential-helpers" + packages = [ + "client", + "credentials", + ] + pruneopts = "UT" + revision = "5241b46610f2491efdf9d1c85f1ddf5b02f6d962" + version = "v0.6.1" + +[[projects]] + digest = "1:811c86996b1ca46729bad2724d4499014c4b9effd05ef8c71b852aad90deb0ce" + name = "github.com/docker/go-connections" + packages = [ + "nat", + "sockets", + "tlsconfig", + ] + pruneopts = "UT" + revision = "7395e3f8aa162843a74ed6d48e79627d9792ac55" + version = "v0.4.0" + [[projects]] branch = "master" digest = "1:2b126e77be4ab4b92cdb3924c87894dd76bf365ba282f358a13133e848aa0059" @@ -711,6 +797,14 @@ revision = "d60099175f88c47cd379c4738d158884749ed235" version = "v1.0.1" +[[projects]] + digest = "1:38ee335aedf4626620f3cf8f605661e71abdcce7b40b38921962beb3980f0a20" + name = "github.com/opencontainers/runc" + packages = ["libcontainer/user"] + pruneopts = "UT" + revision = "baf6536d6259209c3edfa2b22237af82942d3dfa" + version = "v0.1.1" + [[projects]] branch = "master" digest = "1:3bf17a6e6eaa6ad24152148a631d18662f7212e21637c2699bff3369b7f00fa2" @@ -915,9 +1009,11 @@ [[projects]] branch = "master" - digest = "1:599ef9ff10026292c425292ab1d2bb1521cd671fe89a6034df07bf1411daa44b" + digest = "1:91e034b0c63a4c747c6e9dc8285f36dc5fe699a78d34de0a663895e52ff673dd" name = "golang.org/x/crypto" packages = [ + "bcrypt", + "blowfish", "cast5", "ed25519", "ed25519/internal/edwards25519", @@ -938,7 +1034,7 @@ [[projects]] branch = "master" - digest = "1:647b0128e9a9886335bfb6c9a1fc97758b7f846ec42f222933f6fee6730c96e2" + digest = "1:80c256dfc14840e13293d6404b7774e497187bd15a53f943f99bfaef4bbb2e42" name = "golang.org/x/net" packages = [ "bpf", @@ -950,9 +1046,11 @@ "idna", "internal/iana", "internal/socket", + "internal/socks", "internal/timeseries", "ipv4", "ipv6", + "proxy", "publicsuffix", "trace", ] @@ -1678,12 +1776,15 @@ "github.com/asaskevich/govalidator", "github.com/containerd/containerd/reference", "github.com/containerd/containerd/remotes", - "github.com/containerd/containerd/remotes/docker", + "github.com/deislabs/oras/pkg/auth", + "github.com/deislabs/oras/pkg/auth/docker", "github.com/deislabs/oras/pkg/content", "github.com/deislabs/oras/pkg/oras", "github.com/docker/distribution/configuration", "github.com/docker/distribution/registry", + "github.com/docker/distribution/registry/auth/htpasswd", "github.com/docker/distribution/registry/storage/driver/inmemory", + "github.com/docker/docker/pkg/term", "github.com/docker/go-units", "github.com/evanphx/json-patch", "github.com/ghodss/yaml", @@ -1701,6 +1802,7 @@ "github.com/stretchr/testify/assert", "github.com/stretchr/testify/suite", "github.com/xeipuuv/gojsonschema", + "golang.org/x/crypto/bcrypt", "golang.org/x/crypto/openpgp", "golang.org/x/crypto/openpgp/clearsign", "golang.org/x/crypto/openpgp/errors", diff --git a/Gopkg.toml b/Gopkg.toml index fe26c0010..94736bf3c 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -44,7 +44,7 @@ [[constraint]] name = "github.com/deislabs/oras" - version = "~0.3.3" + version = "0.4.0" [[constraint]] name = "github.com/docker/go-units" @@ -76,11 +76,6 @@ branch = "master" source = "https://github.com/dmcgowan/letsencrypt.git" -# https://github.com/bugsnag/bugsnag-go/issues/96 -[[override]] - name = "github.com/bugsnag/bugsnag-go" - version = "=1.3.2" - # gopkg.in is broken # # https://github.com/golang/dep/issues/1760 diff --git a/cmd/helm/chart.go b/cmd/helm/chart.go index dd4af9273..293ab3635 100644 --- a/cmd/helm/chart.go +++ b/cmd/helm/chart.go @@ -18,14 +18,13 @@ package main import ( "io" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" "helm.sh/helm/pkg/action" ) const chartHelp = ` -This command consists of multiple subcommands to interact with charts and registries. +This command consists of multiple subcommands to work with the chart cache. It can be used to push, pull, tag, list, or remove Helm charts. Example usage: @@ -48,8 +47,3 @@ func newChartCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ) return cmd } - -// TODO remove once WARN lines removed from oras or containerd -func init() { - logrus.SetLevel(logrus.ErrorLevel) -} diff --git a/cmd/helm/registry.go b/cmd/helm/registry.go new file mode 100644 index 000000000..1ed885c83 --- /dev/null +++ b/cmd/helm/registry.go @@ -0,0 +1,45 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + + "github.com/spf13/cobra" + + "helm.sh/helm/pkg/action" +) + +const registryHelp = ` +This command consists of multiple subcommands to interact with registries. + +It can be used to login to or logout from a registry. +Example usage: + $ helm registry login [URL] +` + +func newRegistryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "registry", + Short: "login to or logout from a registry", + Long: registryHelp, + } + cmd.AddCommand( + newRegistryLoginCmd(cfg, out), + newRegistryLogoutCmd(cfg, out), + ) + return cmd +} diff --git a/cmd/helm/registry_login.go b/cmd/helm/registry_login.go new file mode 100644 index 000000000..d40b1dd44 --- /dev/null +++ b/cmd/helm/registry_login.go @@ -0,0 +1,134 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/docker/docker/pkg/term" + "github.com/spf13/cobra" + + "helm.sh/helm/cmd/helm/require" + "helm.sh/helm/pkg/action" +) + +const registryLoginDesc = ` +Authenticate to a remote registry. +` + +func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + var usernameOpt, passwordOpt string + var passwordFromStdinOpt bool + + cmd := &cobra.Command{ + Use: "login [host]", + Short: "login to a registry", + Long: registryLoginDesc, + Args: require.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + hostname := args[0] + + username, password, err := getUsernamePassword(usernameOpt, passwordOpt, passwordFromStdinOpt) + if err != nil { + return err + } + + return action.NewRegistryLogin(cfg).Run(out, hostname, username, password) + }, + } + + f := cmd.Flags() + f.StringVarP(&usernameOpt, "username", "u", "", "registry username") + f.StringVarP(&passwordOpt, "password", "p", "", "registry password or identity token") + f.BoolVarP(&passwordFromStdinOpt, "password-stdin", "", false, "read password or identity token from stdin") + + return cmd +} + +// Adapted from https://github.com/deislabs/oras +func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) { + var err error + username := usernameOpt + password := passwordOpt + + if passwordFromStdinOpt { + passwordFromStdin, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return "", "", err + } + password = strings.TrimSuffix(string(passwordFromStdin), "\n") + password = strings.TrimSuffix(password, "\r") + } else if password == "" { + if username == "" { + username, err = readLine("Username: ", false) + if err != nil { + return "", "", err + } + username = strings.TrimSpace(username) + } + if username == "" { + password, err = readLine("Token: ", true) + if err != nil { + return "", "", err + } else if password == "" { + return "", "", errors.New("token required") + } + } else { + password, err = readLine("Password: ", true) + if err != nil { + return "", "", err + } else if password == "" { + return "", "", errors.New("password required") + } + } + } else { + fmt.Fprintln(os.Stderr, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.") + } + + return username, password, nil +} + +// Copied/adapted from https://github.com/deislabs/oras +func readLine(prompt string, silent bool) (string, error) { + fmt.Print(prompt) + if silent { + fd := os.Stdin.Fd() + state, err := term.SaveState(fd) + if err != nil { + return "", err + } + term.DisableEcho(fd, state) + defer term.RestoreTerminal(fd, state) + } + + reader := bufio.NewReader(os.Stdin) + line, _, err := reader.ReadLine() + if err != nil { + return "", err + } + if silent { + fmt.Println() + } + + return string(line), nil +} diff --git a/cmd/helm/registry_logout.go b/cmd/helm/registry_logout.go new file mode 100644 index 000000000..099f4ee7b --- /dev/null +++ b/cmd/helm/registry_logout.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "io" + + "github.com/spf13/cobra" + + "helm.sh/helm/cmd/helm/require" + "helm.sh/helm/pkg/action" +) + +const registryLogoutDesc = ` +Remove credentials stored for a remote registry. +` + +func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "logout [host]", + Short: "logout from a registry", + Long: registryLogoutDesc, + Args: require.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + hostname := args[0] + return action.NewRegistryLogout(cfg).Run(out, hostname) + }, + } +} diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 5a6f43d6e..f2124aba4 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -17,9 +17,11 @@ limitations under the License. package main // import "helm.sh/helm/cmd/helm" import ( + "context" "io" + "path/filepath" - "github.com/containerd/containerd/remotes/docker" + auth "github.com/deislabs/oras/pkg/auth/docker" "github.com/spf13/cobra" "helm.sh/helm/cmd/helm/require" @@ -68,10 +70,23 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string // Add the registry client based on settings // TODO: Move this elsewhere (first, settings.Init() must move) + // TODO: handle errors, dont panic + credentialsFile := filepath.Join(settings.Home.Registry(), registry.CredentialsFileBasename) + client, err := auth.NewClient(credentialsFile) + if err != nil { + panic(err) + } + resolver, err := client.Resolver(context.Background()) + if err != nil { + panic(err) + } actionConfig.RegistryClient = registry.NewClient(®istry.ClientOptions{ Out: out, + Authorizer: registry.Authorizer{ + Client: client, + }, Resolver: registry.Resolver{ - Resolver: docker.NewResolver(docker.ResolverOptions{}), + Resolver: resolver, }, CacheRootDir: settings.Home.Registry(), }) @@ -87,6 +102,9 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string newRepoCmd(out), newSearchCmd(out), newVerifyCmd(out), + + // registry/chart cache commands + newRegistryCmd(actionConfig, out), newChartCmd(actionConfig, out), // release commands diff --git a/pkg/action/registry_login.go b/pkg/action/registry_login.go new file mode 100644 index 000000000..2192d49e8 --- /dev/null +++ b/pkg/action/registry_login.go @@ -0,0 +1,38 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "io" +) + +// RegistryLogin performs a registry login operation. +type RegistryLogin struct { + cfg *Configuration +} + +// NewRegistryLogin creates a new RegistryLogin object with the given configuration. +func NewRegistryLogin(cfg *Configuration) *RegistryLogin { + return &RegistryLogin{ + cfg: cfg, + } +} + +// Run executes the registry login operation +func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string) error { + return a.cfg.RegistryClient.Login(hostname, username, password) +} diff --git a/pkg/action/registry_logout.go b/pkg/action/registry_logout.go new file mode 100644 index 000000000..69add4163 --- /dev/null +++ b/pkg/action/registry_logout.go @@ -0,0 +1,38 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "io" +) + +// RegistryLogout performs a registry login operation. +type RegistryLogout struct { + cfg *Configuration +} + +// NewRegistryLogout creates a new RegistryLogout object with the given configuration. +func NewRegistryLogout(cfg *Configuration) *RegistryLogout { + return &RegistryLogout{ + cfg: cfg, + } +} + +// Run executes the registry logout operation +func (a *RegistryLogout) Run(out io.Writer, hostname string) error { + return a.cfg.RegistryClient.Logout(hostname) +} diff --git a/pkg/engine/funcs.go b/pkg/engine/funcs.go index 2b927872f..936f91d3c 100644 --- a/pkg/engine/funcs.go +++ b/pkg/engine/funcs.go @@ -25,7 +25,7 @@ import ( "github.com/BurntSushi/toml" "github.com/Masterminds/sprig" "github.com/pkg/errors" - "gopkg.in/yaml.v2" + yaml "gopkg.in/yaml.v2" ) // funcMap returns a mapping of all of the functions that Engine has. diff --git a/pkg/registry/authorizer.go b/pkg/registry/authorizer.go new file mode 100644 index 000000000..c601b59d4 --- /dev/null +++ b/pkg/registry/authorizer.go @@ -0,0 +1,28 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry // import "helm.sh/helm/pkg/registry" + +import ( + "github.com/deislabs/oras/pkg/auth" +) + +type ( + // Authorizer handles registry auth operations + Authorizer struct { + auth.Client + } +) diff --git a/pkg/registry/cache.go b/pkg/registry/cache.go index 39dec1467..ccedd1e54 100644 --- a/pkg/registry/cache.go +++ b/pkg/registry/cache.go @@ -29,7 +29,7 @@ import ( "time" orascontent "github.com/deislabs/oras/pkg/content" - "github.com/docker/go-units" + units "github.com/docker/go-units" checksum "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" diff --git a/pkg/registry/client.go b/pkg/registry/client.go index a2244f816..588961d02 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -28,27 +28,34 @@ import ( "helm.sh/helm/pkg/chart" ) +const ( + CredentialsFileBasename = "config.json" +) + type ( // ClientOptions is used to construct a new client ClientOptions struct { Out io.Writer + Authorizer Authorizer Resolver Resolver CacheRootDir string } // Client works with OCI-compliant registries and local Helm chart cache Client struct { - out io.Writer - resolver Resolver - cache *filesystemCache // TODO: something more robust + out io.Writer + authorizer Authorizer + resolver Resolver + cache *filesystemCache // TODO: something more robust } ) // NewClient returns a new registry client with config func NewClient(options *ClientOptions) *Client { return &Client{ - out: options.Out, - resolver: options.Resolver, + out: options.Out, + resolver: options.Resolver, + authorizer: options.Authorizer, cache: &filesystemCache{ out: options.Out, rootDir: options.CacheRootDir, @@ -57,6 +64,26 @@ func NewClient(options *ClientOptions) *Client { } } +// Login logs into a registry +func (c *Client) Login(hostname string, username string, password string) error { + err := c.authorizer.Login(context.Background(), hostname, username, password) + if err != nil { + return err + } + fmt.Fprint(c.out, "Login succeeded\n") + return nil +} + +// Logout logs out of a registry +func (c *Client) Logout(hostname string) error { + err := c.authorizer.Logout(context.Background(), hostname) + if err != nil { + return err + } + fmt.Fprint(c.out, "Logout succeeded\n") + return nil +} + // PushChart uploads a chart to a registry func (c *Client) PushChart(ref *Reference) error { c.setDefaultTag(ref) @@ -65,7 +92,7 @@ func (c *Client) PushChart(ref *Reference) error { if err != nil { return err } - err = oras.Push(context.Background(), c.resolver, ref.String(), c.cache.store, layers) + _, err = oras.Push(context.Background(), c.resolver, ref.String(), c.cache.store, layers) if err != nil { return err } @@ -82,7 +109,7 @@ func (c *Client) PushChart(ref *Reference) error { func (c *Client) PullChart(ref *Reference) error { c.setDefaultTag(ref) fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo) - layers, err := oras.Pull(context.Background(), c.resolver, ref.String(), c.cache.store, KnownMediaTypes()...) + _, layers, err := oras.Pull(context.Background(), c.resolver, ref.String(), c.cache.store, oras.WithAllowedMediaTypes(KnownMediaTypes())) if err != nil { return err } diff --git a/pkg/registry/client_test.go b/pkg/registry/client_test.go index fd6285c15..5b2f06eb5 100644 --- a/pkg/registry/client_test.go +++ b/pkg/registry/client_test.go @@ -21,22 +21,29 @@ import ( "context" "fmt" "io" + "io/ioutil" "net" "os" + "path/filepath" "testing" "time" - "github.com/containerd/containerd/remotes/docker" + auth "github.com/deislabs/oras/pkg/auth/docker" "github.com/docker/distribution/configuration" "github.com/docker/distribution/registry" + _ "github.com/docker/distribution/registry/auth/htpasswd" _ "github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/stretchr/testify/suite" + "golang.org/x/crypto/bcrypt" "helm.sh/helm/pkg/chart" ) var ( - testCacheRootDir = "helm-registry-test" + testCacheRootDir = "helm-registry-test" + testHtpasswdFileBasename = "authtest.htpasswd" + testUsername = "myuser" + testPassword = "mypass" ) type RegistryClientTestSuite struct { @@ -49,28 +56,52 @@ type RegistryClientTestSuite struct { func (suite *RegistryClientTestSuite) SetupSuite() { suite.CacheRootDir = testCacheRootDir + os.RemoveAll(suite.CacheRootDir) + os.Mkdir(suite.CacheRootDir, 0700) - // Init test client var out bytes.Buffer suite.Out = &out + credentialsFile := filepath.Join(suite.CacheRootDir, CredentialsFileBasename) + + client, err := auth.NewClient(credentialsFile) + suite.Nil(err, "no error creating auth client") + + resolver, err := client.Resolver(context.Background()) + suite.Nil(err, "no error creating resolver") + + // Init test client suite.RegistryClient = NewClient(&ClientOptions{ Out: suite.Out, + Authorizer: Authorizer{ + Client: client, + }, Resolver: Resolver{ - Resolver: docker.NewResolver(docker.ResolverOptions{}), + Resolver: resolver, }, CacheRootDir: suite.CacheRootDir, }) + // create htpasswd file (w BCrypt, which is required) + pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) + suite.Nil(err, "no error generating bcrypt password for test htpasswd file") + htpasswdPath := filepath.Join(suite.CacheRootDir, testHtpasswdFileBasename) + err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) + suite.Nil(err, "no error creating test htpasswd file") + // Registry config config := &configuration.Configuration{} port, err := getFreePort() - if err != nil { - suite.Nil(err, "no error finding free port for test registry") - } + suite.Nil(err, "no error finding free port for test registry") suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port) config.HTTP.Addr = fmt.Sprintf(":%d", port) config.HTTP.DrainTimeout = time.Duration(10) * time.Second config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + config.Auth = configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "localhost", + "path": htpasswdPath, + }, + } dockerRegistry, err := registry.NewRegistry(context.Background(), config) suite.Nil(err, "no error creating test registry") @@ -82,7 +113,15 @@ func (suite *RegistryClientTestSuite) TearDownSuite() { os.RemoveAll(suite.CacheRootDir) } -func (suite *RegistryClientTestSuite) Test_0_SaveChart() { +func (suite *RegistryClientTestSuite) Test_0_Login() { + err := suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad") + suite.NotNil(err, "error logging into registry with bad credentials") + + err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword) + suite.Nil(err, "no error logging into registry with good credentials") +} + +func (suite *RegistryClientTestSuite) Test_1_SaveChart() { ref, err := ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) suite.Nil(err) @@ -101,7 +140,7 @@ func (suite *RegistryClientTestSuite) Test_0_SaveChart() { suite.Nil(err) } -func (suite *RegistryClientTestSuite) Test_1_LoadChart() { +func (suite *RegistryClientTestSuite) Test_2_LoadChart() { // non-existent ref ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) @@ -118,7 +157,7 @@ func (suite *RegistryClientTestSuite) Test_1_LoadChart() { suite.Equal("1.2.3", ch.Metadata.Version) } -func (suite *RegistryClientTestSuite) Test_2_PushChart() { +func (suite *RegistryClientTestSuite) Test_3_PushChart() { // non-existent ref ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) @@ -133,7 +172,7 @@ func (suite *RegistryClientTestSuite) Test_2_PushChart() { suite.Nil(err) } -func (suite *RegistryClientTestSuite) Test_3_PullChart() { +func (suite *RegistryClientTestSuite) Test_4_PullChart() { // non-existent ref ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) @@ -148,12 +187,12 @@ func (suite *RegistryClientTestSuite) Test_3_PullChart() { suite.Nil(err) } -func (suite *RegistryClientTestSuite) Test_4_PrintChartTable() { +func (suite *RegistryClientTestSuite) Test_5_PrintChartTable() { err := suite.RegistryClient.PrintChartTable() suite.Nil(err) } -func (suite *RegistryClientTestSuite) Test_5_RemoveChart() { +func (suite *RegistryClientTestSuite) Test_6_RemoveChart() { // non-existent ref ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) @@ -168,6 +207,14 @@ func (suite *RegistryClientTestSuite) Test_5_RemoveChart() { suite.Nil(err) } +func (suite *RegistryClientTestSuite) Test_7_Logout() { + err := suite.RegistryClient.Logout("this-host-aint-real:5000") + suite.NotNil(err, "error logging out of registry that has no entry") + + err = suite.RegistryClient.Logout(suite.DockerRegistryHost) + suite.Nil(err, "no error logging out of registry") +} + func TestRegistryClientTestSuite(t *testing.T) { suite.Run(t, new(RegistryClientTestSuite)) }