Helm 3: registry login/logout (#5597)

* login/logout placeholders

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* use latest oras

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* use docker auth system

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* working login+push

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* working on tests

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* fix typo in htpasswd

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* rename credsfile to config.json

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* add flags for username/password

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* disable logout test broken on linux

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* upgrade to oras 0.4.0

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* re-enable logout test

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* panic for uncaught errors

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>

* move login/logout to new registry subcommand

Signed-off-by: Josh Dolitsky <jdolitsky@gmail.com>
pull/5681/head
Josh Dolitsky 6 years ago committed by GitHub
parent b1ae1acc8b
commit a12a396aab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

124
Gopkg.lock generated

@ -91,6 +91,22 @@
revision = "b4f55832432b95a611cf1495272b5c8e24952a1a" revision = "b4f55832432b95a611cf1495272b5c8e24952a1a"
version = "v1.13.0" 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]] [[projects]]
digest = "1:d1665c44bd5db19aaee18d1b6233c99b0b9a986e8bccb24ef54747547a48027f" digest = "1:d1665c44bd5db19aaee18d1b6233c99b0b9a986e8bccb24ef54747547a48027f"
name = "github.com/PuerkitoBio/purell" name = "github.com/PuerkitoBio/purell"
@ -185,7 +201,7 @@
revision = "bf70f2a70fb1b1f36d90d671a72795984eab0fcb" revision = "bf70f2a70fb1b1f36d90d671a72795984eab0fcb"
[[projects]] [[projects]]
digest = "1:1872596d45cb52913fcdea90d468f3e57435959e9c0d99ccb316ee76de341313" digest = "1:37f8940c4d3c41536ea882b1ca3498e403c04892dfc34bd0d670ed9eafccda9a"
name = "github.com/containerd/containerd" name = "github.com/containerd/containerd"
packages = [ packages = [
"content", "content",
@ -198,8 +214,16 @@
"remotes/docker", "remotes/docker",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "9b32062dc1f5a7c2564315c269b5059754f12b9d" revision = "894b81a4b802e4eb2a91d1ce216b8817763c29fb"
version = "v1.2.1" version = "v1.2.6"
[[projects]]
branch = "master"
digest = "1:e48c63e818c67fbf3d7afe20bba33134ab1a5bf384847385384fd027652a5a96"
name = "github.com/containerd/continuity"
packages = ["pathdriver"]
pruneopts = "UT"
revision = "004b46473808b3e7a4a3049c20e4376c91eb966d"
[[projects]] [[projects]]
digest = "1:7cb4fdca4c251b3ef8027c90ea35f70c7b661a593b9eeae34753c65499098bb1" digest = "1:7cb4fdca4c251b3ef8027c90ea35f70c7b661a593b9eeae34753c65499098bb1"
@ -218,15 +242,17 @@
version = "v1.1.1" version = "v1.1.1"
[[projects]] [[projects]]
digest = "1:8285cd51b86f5c3af447ad1db7c8572578a422cf9a50f1f07eea1d021151044f" digest = "1:82158435e282da9b23bb1188487fe1c68b17a54ed9dcd557ab6204782ad3ff92"
name = "github.com/deislabs/oras" name = "github.com/deislabs/oras"
packages = [ packages = [
"pkg/auth",
"pkg/auth/docker",
"pkg/content", "pkg/content",
"pkg/oras", "pkg/oras",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "e8a1fa6ff9a507b99eedd45745959e8c5b826d9f" revision = "9f7669048990b0d0c186985737e6a6c3bb3f7ecc"
version = "v0.3.3" version = "v0.4.0"
[[projects]] [[projects]]
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55" digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
@ -237,7 +263,20 @@
version = "v3.2.0" version = "v3.2.0"
[[projects]] [[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" name = "github.com/docker/distribution"
packages = [ packages = [
".", ".",
@ -258,6 +297,7 @@
"registry/api/errcode", "registry/api/errcode",
"registry/api/v2", "registry/api/v2",
"registry/auth", "registry/auth",
"registry/auth/htpasswd",
"registry/client", "registry/client",
"registry/client/auth", "registry/client/auth",
"registry/client/auth/challenge", "registry/client/auth/challenge",
@ -286,15 +326,61 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:8da8bb2b12c31c632e96ca6f15666a36c36cd390326b6c5e1c5e309cf4b5419a" digest = "1:b5be0d9940d8fa3ff7df4949a8e8c47a7f93ea8251239ad074e1a6b0db55876a"
name = "github.com/docker/docker" name = "github.com/docker/docker"
packages = [ 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",
"pkg/term/windows", "pkg/term/windows",
"registry",
"registry/resumable",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "2cb26cfe9cbf8a64c5046c74d65f4528b22e67f4" 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]] [[projects]]
branch = "master" branch = "master"
digest = "1:2b126e77be4ab4b92cdb3924c87894dd76bf365ba282f358a13133e848aa0059" digest = "1:2b126e77be4ab4b92cdb3924c87894dd76bf365ba282f358a13133e848aa0059"
@ -711,6 +797,14 @@
revision = "d60099175f88c47cd379c4738d158884749ed235" revision = "d60099175f88c47cd379c4738d158884749ed235"
version = "v1.0.1" 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]] [[projects]]
branch = "master" branch = "master"
digest = "1:3bf17a6e6eaa6ad24152148a631d18662f7212e21637c2699bff3369b7f00fa2" digest = "1:3bf17a6e6eaa6ad24152148a631d18662f7212e21637c2699bff3369b7f00fa2"
@ -915,9 +1009,11 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:599ef9ff10026292c425292ab1d2bb1521cd671fe89a6034df07bf1411daa44b" digest = "1:91e034b0c63a4c747c6e9dc8285f36dc5fe699a78d34de0a663895e52ff673dd"
name = "golang.org/x/crypto" name = "golang.org/x/crypto"
packages = [ packages = [
"bcrypt",
"blowfish",
"cast5", "cast5",
"ed25519", "ed25519",
"ed25519/internal/edwards25519", "ed25519/internal/edwards25519",
@ -938,7 +1034,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:647b0128e9a9886335bfb6c9a1fc97758b7f846ec42f222933f6fee6730c96e2" digest = "1:80c256dfc14840e13293d6404b7774e497187bd15a53f943f99bfaef4bbb2e42"
name = "golang.org/x/net" name = "golang.org/x/net"
packages = [ packages = [
"bpf", "bpf",
@ -950,9 +1046,11 @@
"idna", "idna",
"internal/iana", "internal/iana",
"internal/socket", "internal/socket",
"internal/socks",
"internal/timeseries", "internal/timeseries",
"ipv4", "ipv4",
"ipv6", "ipv6",
"proxy",
"publicsuffix", "publicsuffix",
"trace", "trace",
] ]
@ -1678,12 +1776,15 @@
"github.com/asaskevich/govalidator", "github.com/asaskevich/govalidator",
"github.com/containerd/containerd/reference", "github.com/containerd/containerd/reference",
"github.com/containerd/containerd/remotes", "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/content",
"github.com/deislabs/oras/pkg/oras", "github.com/deislabs/oras/pkg/oras",
"github.com/docker/distribution/configuration", "github.com/docker/distribution/configuration",
"github.com/docker/distribution/registry", "github.com/docker/distribution/registry",
"github.com/docker/distribution/registry/auth/htpasswd",
"github.com/docker/distribution/registry/storage/driver/inmemory", "github.com/docker/distribution/registry/storage/driver/inmemory",
"github.com/docker/docker/pkg/term",
"github.com/docker/go-units", "github.com/docker/go-units",
"github.com/evanphx/json-patch", "github.com/evanphx/json-patch",
"github.com/ghodss/yaml", "github.com/ghodss/yaml",
@ -1701,6 +1802,7 @@
"github.com/stretchr/testify/assert", "github.com/stretchr/testify/assert",
"github.com/stretchr/testify/suite", "github.com/stretchr/testify/suite",
"github.com/xeipuuv/gojsonschema", "github.com/xeipuuv/gojsonschema",
"golang.org/x/crypto/bcrypt",
"golang.org/x/crypto/openpgp", "golang.org/x/crypto/openpgp",
"golang.org/x/crypto/openpgp/clearsign", "golang.org/x/crypto/openpgp/clearsign",
"golang.org/x/crypto/openpgp/errors", "golang.org/x/crypto/openpgp/errors",

@ -44,7 +44,7 @@
[[constraint]] [[constraint]]
name = "github.com/deislabs/oras" name = "github.com/deislabs/oras"
version = "~0.3.3" version = "0.4.0"
[[constraint]] [[constraint]]
name = "github.com/docker/go-units" name = "github.com/docker/go-units"
@ -76,11 +76,6 @@
branch = "master" branch = "master"
source = "https://github.com/dmcgowan/letsencrypt.git" 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 # gopkg.in is broken
# #
# https://github.com/golang/dep/issues/1760 # https://github.com/golang/dep/issues/1760

@ -18,14 +18,13 @@ package main
import ( import (
"io" "io"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
) )
const chartHelp = ` 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. It can be used to push, pull, tag, list, or remove Helm charts.
Example usage: Example usage:
@ -48,8 +47,3 @@ func newChartCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
) )
return cmd return cmd
} }
// TODO remove once WARN lines removed from oras or containerd
func init() {
logrus.SetLevel(logrus.ErrorLevel)
}

@ -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
}

@ -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
}

@ -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)
},
}
}

@ -17,9 +17,11 @@ limitations under the License.
package main // import "helm.sh/helm/cmd/helm" package main // import "helm.sh/helm/cmd/helm"
import ( import (
"context"
"io" "io"
"path/filepath"
"github.com/containerd/containerd/remotes/docker" auth "github.com/deislabs/oras/pkg/auth/docker"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require" "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 // Add the registry client based on settings
// TODO: Move this elsewhere (first, settings.Init() must move) // 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(&registry.ClientOptions{ actionConfig.RegistryClient = registry.NewClient(&registry.ClientOptions{
Out: out, Out: out,
Authorizer: registry.Authorizer{
Client: client,
},
Resolver: registry.Resolver{ Resolver: registry.Resolver{
Resolver: docker.NewResolver(docker.ResolverOptions{}), Resolver: resolver,
}, },
CacheRootDir: settings.Home.Registry(), CacheRootDir: settings.Home.Registry(),
}) })
@ -87,6 +102,9 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
newRepoCmd(out), newRepoCmd(out),
newSearchCmd(out), newSearchCmd(out),
newVerifyCmd(out), newVerifyCmd(out),
// registry/chart cache commands
newRegistryCmd(actionConfig, out),
newChartCmd(actionConfig, out), newChartCmd(actionConfig, out),
// release commands // release commands

@ -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)
}

@ -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)
}

@ -25,7 +25,7 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/Masterminds/sprig" "github.com/Masterminds/sprig"
"github.com/pkg/errors" "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. // funcMap returns a mapping of all of the functions that Engine has.

@ -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
}
)

@ -29,7 +29,7 @@ import (
"time" "time"
orascontent "github.com/deislabs/oras/pkg/content" orascontent "github.com/deislabs/oras/pkg/content"
"github.com/docker/go-units" units "github.com/docker/go-units"
checksum "github.com/opencontainers/go-digest" checksum "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"

@ -28,27 +28,34 @@ import (
"helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart"
) )
const (
CredentialsFileBasename = "config.json"
)
type ( type (
// ClientOptions is used to construct a new client // ClientOptions is used to construct a new client
ClientOptions struct { ClientOptions struct {
Out io.Writer Out io.Writer
Authorizer Authorizer
Resolver Resolver Resolver Resolver
CacheRootDir string CacheRootDir string
} }
// Client works with OCI-compliant registries and local Helm chart cache // Client works with OCI-compliant registries and local Helm chart cache
Client struct { Client struct {
out io.Writer out io.Writer
resolver Resolver authorizer Authorizer
cache *filesystemCache // TODO: something more robust resolver Resolver
cache *filesystemCache // TODO: something more robust
} }
) )
// NewClient returns a new registry client with config // NewClient returns a new registry client with config
func NewClient(options *ClientOptions) *Client { func NewClient(options *ClientOptions) *Client {
return &Client{ return &Client{
out: options.Out, out: options.Out,
resolver: options.Resolver, resolver: options.Resolver,
authorizer: options.Authorizer,
cache: &filesystemCache{ cache: &filesystemCache{
out: options.Out, out: options.Out,
rootDir: options.CacheRootDir, 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 // PushChart uploads a chart to a registry
func (c *Client) PushChart(ref *Reference) error { func (c *Client) PushChart(ref *Reference) error {
c.setDefaultTag(ref) c.setDefaultTag(ref)
@ -65,7 +92,7 @@ func (c *Client) PushChart(ref *Reference) error {
if err != nil { if err != nil {
return err 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 { if err != nil {
return err return err
} }
@ -82,7 +109,7 @@ func (c *Client) PushChart(ref *Reference) error {
func (c *Client) PullChart(ref *Reference) error { func (c *Client) PullChart(ref *Reference) error {
c.setDefaultTag(ref) c.setDefaultTag(ref)
fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo) 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 { if err != nil {
return err return err
} }

@ -21,22 +21,29 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"os" "os"
"path/filepath"
"testing" "testing"
"time" "time"
"github.com/containerd/containerd/remotes/docker" auth "github.com/deislabs/oras/pkg/auth/docker"
"github.com/docker/distribution/configuration" "github.com/docker/distribution/configuration"
"github.com/docker/distribution/registry" "github.com/docker/distribution/registry"
_ "github.com/docker/distribution/registry/auth/htpasswd"
_ "github.com/docker/distribution/registry/storage/driver/inmemory" _ "github.com/docker/distribution/registry/storage/driver/inmemory"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"golang.org/x/crypto/bcrypt"
"helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart"
) )
var ( var (
testCacheRootDir = "helm-registry-test" testCacheRootDir = "helm-registry-test"
testHtpasswdFileBasename = "authtest.htpasswd"
testUsername = "myuser"
testPassword = "mypass"
) )
type RegistryClientTestSuite struct { type RegistryClientTestSuite struct {
@ -49,28 +56,52 @@ type RegistryClientTestSuite struct {
func (suite *RegistryClientTestSuite) SetupSuite() { func (suite *RegistryClientTestSuite) SetupSuite() {
suite.CacheRootDir = testCacheRootDir suite.CacheRootDir = testCacheRootDir
os.RemoveAll(suite.CacheRootDir)
os.Mkdir(suite.CacheRootDir, 0700)
// Init test client
var out bytes.Buffer var out bytes.Buffer
suite.Out = &out 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{ suite.RegistryClient = NewClient(&ClientOptions{
Out: suite.Out, Out: suite.Out,
Authorizer: Authorizer{
Client: client,
},
Resolver: Resolver{ Resolver: Resolver{
Resolver: docker.NewResolver(docker.ResolverOptions{}), Resolver: resolver,
}, },
CacheRootDir: suite.CacheRootDir, 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 // Registry config
config := &configuration.Configuration{} config := &configuration.Configuration{}
port, err := getFreePort() 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) suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port)
config.HTTP.Addr = fmt.Sprintf(":%d", port) config.HTTP.Addr = fmt.Sprintf(":%d", port)
config.HTTP.DrainTimeout = time.Duration(10) * time.Second config.HTTP.DrainTimeout = time.Duration(10) * time.Second
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} 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) dockerRegistry, err := registry.NewRegistry(context.Background(), config)
suite.Nil(err, "no error creating test registry") suite.Nil(err, "no error creating test registry")
@ -82,7 +113,15 @@ func (suite *RegistryClientTestSuite) TearDownSuite() {
os.RemoveAll(suite.CacheRootDir) 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)) ref, err := ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
suite.Nil(err) suite.Nil(err)
@ -101,7 +140,7 @@ func (suite *RegistryClientTestSuite) Test_0_SaveChart() {
suite.Nil(err) suite.Nil(err)
} }
func (suite *RegistryClientTestSuite) Test_1_LoadChart() { func (suite *RegistryClientTestSuite) Test_2_LoadChart() {
// non-existent ref // non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) 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) suite.Equal("1.2.3", ch.Metadata.Version)
} }
func (suite *RegistryClientTestSuite) Test_2_PushChart() { func (suite *RegistryClientTestSuite) Test_3_PushChart() {
// non-existent ref // non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) 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) suite.Nil(err)
} }
func (suite *RegistryClientTestSuite) Test_3_PullChart() { func (suite *RegistryClientTestSuite) Test_4_PullChart() {
// non-existent ref // non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) 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) suite.Nil(err)
} }
func (suite *RegistryClientTestSuite) Test_4_PrintChartTable() { func (suite *RegistryClientTestSuite) Test_5_PrintChartTable() {
err := suite.RegistryClient.PrintChartTable() err := suite.RegistryClient.PrintChartTable()
suite.Nil(err) suite.Nil(err)
} }
func (suite *RegistryClientTestSuite) Test_5_RemoveChart() { func (suite *RegistryClientTestSuite) Test_6_RemoveChart() {
// non-existent ref // non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) 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) 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) { func TestRegistryClientTestSuite(t *testing.T) {
suite.Run(t, new(RegistryClientTestSuite)) suite.Run(t, new(RegistryClientTestSuite))
} }

Loading…
Cancel
Save