Merge pull request #9782 from bloodorangeio/hip-6

Implement changes proposed in HIP 6: OCI Support
pull/9707/head
Matt Farina 3 years ago committed by GitHub
commit 601bed2142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,49 +0,0 @@
/*
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/v3/pkg/action"
)
const chartHelp = `
This command consists of multiple subcommands to work with the chart cache.
The subcommands can be used to push, pull, tag, list, or remove Helm charts.
`
func newChartCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "chart",
Short: "push, pull, tag, or remove Helm charts",
Long: chartHelp,
Hidden: !FeatureGateOCI.IsEnabled(),
PersistentPreRunE: checkOCIFeatureGate(),
}
cmd.AddCommand(
newChartListCmd(cfg, out),
newChartExportCmd(cfg, out),
newChartPullCmd(cfg, out),
newChartPushCmd(cfg, out),
newChartRemoveCmd(cfg, out),
newChartSaveCmd(cfg, out),
)
return cmd
}

@ -1,55 +0,0 @@
/*
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/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
)
const chartExportDesc = `
Export a chart stored in local registry cache.
This will create a new directory with the name of
the chart, in a format that developers can modify
and check into source control if desired.
`
func newChartExportCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewChartExport(cfg)
cmd := &cobra.Command{
Use: "export [ref]",
Short: "export a chart to directory",
Long: chartExportDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
ref := args[0]
return client.Run(out, ref)
},
}
f := cmd.Flags()
f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.")
return cmd
}

@ -1,48 +0,0 @@
/*
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/v3/pkg/action"
)
const chartListDesc = `
List all charts in the local registry cache.
Charts are sorted by ref name, alphabetically.
`
func newChartListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
chartList := action.NewChartList(cfg)
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "list all saved charts",
Long: chartListDesc,
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
return chartList.Run(out)
},
}
f := cmd.Flags()
f.UintVar(&chartList.ColumnWidth, "max-col-width", 60, "maximum column width for output table")
return cmd
}

@ -1,46 +0,0 @@
/*
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/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
)
const chartPullDesc = `
Download a chart from a remote registry.
This will store the chart in the local registry cache to be used later.
`
func newChartPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "pull [ref]",
Short: "pull a chart from remote",
Long: chartPullDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
ref := args[0]
return action.NewChartPull(cfg).Run(out, ref)
},
}
}

@ -1,48 +0,0 @@
/*
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/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
)
const chartPushDesc = `
Upload a chart to a remote registry.
Note: the ref must already exist in the local registry cache.
Must first run "helm chart save" or "helm chart pull".
`
func newChartPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "push [ref]",
Short: "push a chart to remote",
Long: chartPushDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
ref := args[0]
return action.NewChartPush(cfg).Run(out, ref)
},
}
}

@ -1,50 +0,0 @@
/*
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/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
)
const chartRemoveDesc = `
Remove a chart from the local registry cache.
Note: the chart content will still exist in the cache,
but it will no longer appear in "helm chart list".
To remove all unlinked content, please run "helm chart prune". (TODO)
`
func newChartRemoveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "remove [ref]",
Aliases: []string{"rm"},
Short: "remove a chart",
Long: chartRemoveDesc,
Args: require.MinimumNArgs(1),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
ref := args[0]
return action.NewChartRemove(cfg).Run(out, ref)
},
}
}

@ -1,61 +0,0 @@
/*
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"
"path/filepath"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
)
const chartSaveDesc = `
Store a copy of chart in local registry cache.
Note: modifying the chart after this operation will
not change the item as it exists in the cache.
`
func newChartSaveCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return &cobra.Command{
Use: "save [path] [ref]",
Short: "save a chart directory",
Long: chartSaveDesc,
Args: require.MinimumNArgs(2),
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
path := args[0]
ref := args[1]
path, err := filepath.Abs(path)
if err != nil {
return err
}
ch, err := loader.Load(path)
if err != nil {
return err
}
return action.NewChartSave(cfg).Run(out, ch, ref)
},
}
}

@ -45,7 +45,7 @@ func TestDependencyBuildCmd(t *testing.T) {
ociChartName := "oci-depending-chart"
c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil {
if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
t.Fatal(err)
}
ociSrv.Run(t, repotest.WithDependingChart(c))
@ -136,6 +136,9 @@ func TestDependencyBuildCmd(t *testing.T) {
}
// OCI dependencies
if err := chartutil.SaveDir(c, dir()); err != nil {
t.Fatal(err)
}
cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
dir(ociChartName),
dir("repositories.yaml"),

@ -46,7 +46,7 @@ func TestDependencyUpdateCmd(t *testing.T) {
ociChartName := "oci-depending-chart"
c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL)
if err := chartutil.SaveDir(c, ociSrv.Dir); err != nil {
if _, err := chartutil.Save(c, ociSrv.Dir); err != nil {
t.Fatal(err)
}
ociSrv.Run(t, repotest.WithDependingChart(c))
@ -133,6 +133,9 @@ func TestDependencyUpdateCmd(t *testing.T) {
}
// test for OCI charts
if err := chartutil.SaveDir(c, dir()); err != nil {
t.Fatal(err)
}
cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json",
dir(ociChartName),
dir("repositories.yaml"),

@ -182,6 +182,10 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
}
client.ReleaseName = name
if err := checkOCI(chart); err != nil {
return nil, err
}
cp, err := client.ChartPathOptions.LocateChart(chart, settings)
if err != nil {
return nil, err

@ -20,7 +20,6 @@ import (
"fmt"
"io"
"log"
"strings"
"github.com/spf13/cobra"
@ -65,10 +64,8 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Version = ">0.0.0-0"
}
if strings.HasPrefix(args[0], "oci://") {
if !FeatureGateOCI.IsEnabled() {
return FeatureGateOCI.Error()
}
if err := checkOCI(args[0]); err != nil {
return err
}
for i := 0; i < len(args); i++ {

@ -0,0 +1,61 @@
/*
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 (
"fmt"
"io"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action"
"helm.sh/helm/v3/pkg/action"
)
const pushDesc = `
Upload a chart to a registry.
If the chart has an associated provenance file,
it will also be uploaded.
`
func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := experimental.NewPushWithOpts(experimental.WithPushConfig(cfg))
cmd := &cobra.Command{
Use: "push [chart] [remote]",
Short: "push a chart to remote",
Long: pushDesc,
Hidden: !FeatureGateOCI.IsEnabled(),
PersistentPreRunE: checkOCIFeatureGate(),
Args: require.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
chartRef := args[0]
remote := args[1]
client.Settings = settings
output, err := client.Run(chartRef, remote)
if err != nil {
return err
}
fmt.Fprint(out, output)
return nil
},
}
return cmd
}

@ -29,6 +29,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action"
"helm.sh/helm/v3/pkg/action"
)
@ -54,7 +55,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
return err
}
return action.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt)
return experimental.NewRegistryLogin(cfg).Run(out, hostname, username, password, insecureOpt)
},
}
@ -67,7 +68,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
return cmd
}
// Adapted from https://oras.land/oras-go
// Adapted from https://github.com/oras-project/oras
func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
var err error
username := usernameOpt
@ -110,7 +111,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd
return username, password, nil
}
// Copied/adapted from https://oras.land/oras-go
// Copied/adapted from https://github.com/oras-project/oras
func readLine(prompt string, silent bool) (string, error) {
fmt.Print(prompt)
if silent {

@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
experimental "helm.sh/helm/v3/internal/experimental/action"
"helm.sh/helm/v3/pkg/action"
)
@ -38,7 +39,7 @@ func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma
Hidden: !FeatureGateOCI.IsEnabled(),
RunE: func(cmd *cobra.Command, args []string) error {
hostname := args[0]
return action.NewRegistryLogout(cfg).Run(out, hostname)
return experimental.NewRegistryLogout(cfg).Run(out, hostname)
},
}
}

@ -200,7 +200,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
// Add *experimental* subcommands
cmd.AddCommand(
newRegistryCmd(actionConfig, out),
newChartCmd(actionConfig, out),
newPushCmd(actionConfig, out),
)
// Find and add plugins
@ -262,3 +262,12 @@ func checkForExpiredRepos(repofile string) {
}
}
// When dealing with OCI-based charts, ensure that the user has
// enabled the experimental feature gate prior to continuing
func checkOCI(ref string) error {
if registry.IsOCI(ref) && !FeatureGateOCI.IsEnabled() {
return FeatureGateOCI.Error()
}
return nil
}

@ -198,6 +198,10 @@ func runShow(args []string, client *action.Show) (string, error) {
client.Version = ">0.0.0-0"
}
if err := checkOCI(args[0]); err != nil {
return "", err
}
cp, err := client.ChartPathOptions.LocateChart(args[0], settings)
if err != nil {
return "", err

@ -83,6 +83,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return nil, cobra.ShellCompDirectiveNoFileComp
},
RunE: func(cmd *cobra.Command, args []string) error {
if err := checkOCI(args[1]); err != nil {
return err
}
client.Namespace = settings.Namespace()
// Fixes #7002 - Support reading values from STDIN for `upgrade` command

@ -14,7 +14,6 @@ require (
github.com/cyphar/filepath-securejoin v0.2.2
github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
github.com/docker/go-units v0.4.0
github.com/evanphx/json-patch v4.11.0+incompatible
github.com/gobwas/glob v0.2.3
github.com/gofrs/flock v0.8.0
@ -23,7 +22,6 @@ require (
github.com/lib/pq v1.10.0
github.com/mattn/go-shellwords v1.0.11
github.com/mitchellh/copystructure v1.1.1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.1
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
github.com/pkg/errors v0.9.1
@ -45,7 +43,6 @@ require (
k8s.io/klog/v2 v2.9.0
k8s.io/kubectl v0.22.1
oras.land/oras-go v0.4.0
rsc.io/letsencrypt v0.0.3 // indirect
sigs.k8s.io/yaml v1.2.0
)
replace github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d

@ -308,8 +308,10 @@ github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3/go.mo
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/cli v20.10.7+incompatible h1:pv/3NqibQKphWZiAskMzdz8w0PRbtTaEB+f6NwdU7Is=
github.com/docker/cli v20.10.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v0.0.0-20191216044856-a8371794149d h1:jC8tT/S0OGx2cswpeUTn4gOIea8P08lD3VFQT0cOZ50=
github.com/docker/distribution v0.0.0-20191216044856-a8371794149d/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8=
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ=
@ -1495,6 +1497,8 @@ k8s.io/utils v0.0.0-20210707171843-4b05e18ac7d9/go.mod h1:jPW/WVKK9YHAvNhRxK0md/
oras.land/oras-go v0.4.0 h1:u6+7D+raZDYHwlz/uOwNANiRmyYDSSMW7A9E1xXycUQ=
oras.land/oras-go v0.4.0/go.mod h1:VJcU+VE4rkclUbum5C0O7deEZbBYnsnpbGSACwTjOcg=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/letsencrypt v0.0.3 h1:H7xDfhkaFFSYEJlKeq38RwX2jYcnTeHuDQyT+mMNMwM=
rsc.io/letsencrypt v0.0.3/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=

@ -0,0 +1,71 @@
/*
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 (
"strings"
"helm.sh/helm/v3/internal/experimental/pusher"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/internal/experimental/uploader"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
)
// Push is the action for uploading a chart.
//
// It provides the implementation of 'helm push'.
type Push struct {
Settings *cli.EnvSettings
cfg *action.Configuration
}
// PushOpt is a type of function that sets options for a push action.
type PushOpt func(*Push)
// WithPushConfig sets the cfg field on the push configuration object.
func WithPushConfig(cfg *action.Configuration) PushOpt {
return func(p *Push) {
p.cfg = cfg
}
}
// NewPushWithOpts creates a new push, with configuration options.
func NewPushWithOpts(opts ...PushOpt) *Push {
p := &Push{}
for _, fn := range opts {
fn(p)
}
return p
}
// Run executes 'helm push' against the given chart archive.
func (p *Push) Run(chartRef string, remote string) (string, error) {
var out strings.Builder
c := uploader.ChartUploader{
Out: &out,
Pushers: pusher.All(p.Settings),
Options: []pusher.Option{},
}
if registry.IsOCI(remote) {
c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient))
}
return out.String(), c.UploadTo(chartRef, remote)
}

@ -18,15 +18,18 @@ package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/action"
)
// RegistryLogin performs a registry login operation.
type RegistryLogin struct {
cfg *Configuration
cfg *action.Configuration
}
// NewRegistryLogin creates a new RegistryLogin object with the given configuration.
func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
func NewRegistryLogin(cfg *action.Configuration) *RegistryLogin {
return &RegistryLogin{
cfg: cfg,
}
@ -34,5 +37,8 @@ func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
// Run executes the registry login operation
func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error {
return a.cfg.RegistryClient.Login(hostname, username, password, insecure)
return a.cfg.RegistryClient.Login(
hostname,
registry.LoginOptBasicAuth(username, password),
registry.LoginOptInsecure(insecure))
}

@ -18,15 +18,17 @@ package action
import (
"io"
"helm.sh/helm/v3/pkg/action"
)
// RegistryLogout performs a registry login operation.
type RegistryLogout struct {
cfg *Configuration
cfg *action.Configuration
}
// NewRegistryLogout creates a new RegistryLogout object with the given configuration.
func NewRegistryLogout(cfg *Configuration) *RegistryLogout {
func NewRegistryLogout(cfg *action.Configuration) *RegistryLogout {
return &RegistryLogout{
cfg: cfg,
}

@ -1,11 +1,10 @@
/*
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
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,
@ -14,15 +13,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"github.com/containerd/containerd/remotes"
)
type (
// Resolver provides remotes based on a locator
Resolver struct {
remotes.Resolver
}
)
/*
Package pusher provides a generalized tool for uploading data by scheme.
This provides a method by which the plugin system can load arbitrary protocol
handlers based upon a URL scheme.
*/
package pusher

@ -0,0 +1,104 @@
/*
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 pusher
import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart/loader"
)
// OCIPusher is the default OCI backend handler
type OCIPusher struct {
opts options
}
// Push performs a Push from repo.Pusher.
func (pusher *OCIPusher) Push(chartRef, href string, options ...Option) error {
for _, opt := range options {
opt(&pusher.opts)
}
return pusher.push(chartRef, href)
}
func (pusher *OCIPusher) push(chartRef, href string) error {
stat, err := os.Stat(chartRef)
if err != nil {
if os.IsNotExist(err) {
return errors.Errorf("%s: no such file", chartRef)
}
return err
}
if stat.IsDir() {
return errors.New("cannot push directory, must provide chart archive (.tgz)")
}
meta, err := loader.Load(chartRef)
if err != nil {
return err
}
client := pusher.opts.registryClient
chartBytes, err := ioutil.ReadFile(chartRef)
if err != nil {
return err
}
var pushOpts []registry.PushOption
provRef := fmt.Sprintf("%s.prov", chartRef)
if _, err := os.Stat(provRef); err == nil {
provBytes, err := ioutil.ReadFile(provRef)
if err != nil {
return err
}
pushOpts = append(pushOpts, registry.PushOptProvData(provBytes))
}
ref := fmt.Sprintf("%s:%s",
path.Join(strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)), meta.Metadata.Name),
meta.Metadata.Version)
_, err = client.Push(chartBytes, ref, pushOpts...)
return err
}
// NewOCIPusher constructs a valid OCI client as a Pusher
func NewOCIPusher(ops ...Option) (Pusher, error) {
registryClient, err := registry.NewClient()
if err != nil {
return nil, err
}
client := OCIPusher{
opts: options{
registryClient: registryClient,
},
}
for _, opt := range ops {
opt(&client.opts)
}
return &client, nil
}

@ -1,11 +1,10 @@
/*
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
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,
@ -14,16 +13,24 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
package pusher
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestConstants(t *testing.T) {
knownMediaTypes := KnownMediaTypes()
assert.Contains(t, knownMediaTypes, HelmChartConfigMediaType)
assert.Contains(t, knownMediaTypes, HelmChartContentLayerMediaType)
func TestNewOCIPusher(t *testing.T) {
testfn := func(ops *options) {
if ops.registryClient == nil {
t.Fatalf("the OCIPusher's registryClient should not be null")
}
}
p, err := NewOCIPusher(testfn)
if p == nil {
t.Error("NewOCIPusher returned nil")
}
if err != nil {
t.Error(err)
}
}

@ -0,0 +1,95 @@
/*
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 pusher
import (
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/cli"
)
// options are generic parameters to be provided to the pusher during instantiation.
//
// Pushers may or may not ignore these parameters as they are passed in.
type options struct {
registryClient *registry.Client
}
// Option allows specifying various settings configurable by the user for overriding the defaults
// used when performing Push operations with the Pusher.
type Option func(*options)
// WithRegistryClient sets the registryClient option.
func WithRegistryClient(client *registry.Client) Option {
return func(opts *options) {
opts.registryClient = client
}
}
// Pusher is an interface to support upload to the specified URL.
type Pusher interface {
// Push file content by url string
Push(chartRef, url string, options ...Option) error
}
// Constructor is the function for every pusher which creates a specific instance
// according to the configuration
type Constructor func(options ...Option) (Pusher, error)
// Provider represents any pusher and the schemes that it supports.
type Provider struct {
Schemes []string
New Constructor
}
// Provides returns true if the given scheme is supported by this Provider.
func (p Provider) Provides(scheme string) bool {
for _, i := range p.Schemes {
if i == scheme {
return true
}
}
return false
}
// Providers is a collection of Provider objects.
type Providers []Provider
// ByScheme returns a Provider that handles the given scheme.
//
// If no provider handles this scheme, this will return an error.
func (p Providers) ByScheme(scheme string) (Pusher, error) {
for _, pp := range p {
if pp.Provides(scheme) {
return pp.New()
}
}
return nil, errors.Errorf("scheme %q not supported", scheme)
}
var ociProvider = Provider{
Schemes: []string{registry.OCIScheme},
New: NewOCIPusher,
}
// All finds all of the registered pushers as a list of Provider instances.
// Currently, just the built-in pushers are collected.
func All(settings *cli.EnvSettings) Providers {
result := Providers{ociProvider}
return result
}

@ -0,0 +1,68 @@
/*
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 pusher
import (
"testing"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/cli"
)
func TestProvider(t *testing.T) {
p := Provider{
[]string{"one", "three"},
func(_ ...Option) (Pusher, error) { return nil, nil },
}
if !p.Provides("three") {
t.Error("Expected provider to provide three")
}
}
func TestProviders(t *testing.T) {
ps := Providers{
{[]string{"one", "three"}, func(_ ...Option) (Pusher, error) { return nil, nil }},
{[]string{"two", "four"}, func(_ ...Option) (Pusher, error) { return nil, nil }},
}
if _, err := ps.ByScheme("one"); err != nil {
t.Error(err)
}
if _, err := ps.ByScheme("four"); err != nil {
t.Error(err)
}
if _, err := ps.ByScheme("five"); err == nil {
t.Error("Did not expect handler for five")
}
}
func TestAll(t *testing.T) {
env := cli.New()
all := All(env)
if len(all) != 1 {
t.Errorf("expected 1 provider (OCI), got %d", len(all))
}
}
func TestByScheme(t *testing.T) {
env := cli.New()
g := All(env)
if _, err := g.ByScheme(registry.OCIScheme); err != nil {
t.Error(err)
}
}

@ -1,368 +0,0 @@
/*
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/v3/internal/experimental/registry"
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
digest "github.com/opencontainers/go-digest"
specs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
orascontent "oras.land/oras-go/pkg/content"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
)
const (
// CacheRootDir is the root directory for a cache
CacheRootDir = "cache"
)
type (
// Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout
Cache struct {
debug bool
out io.Writer
rootDir string
ociStore *orascontent.OCIStore
memoryStore *orascontent.Memorystore
}
// CacheRefSummary contains as much info as available describing a chart reference in cache
// Note: fields here are sorted by the order in which they are set in FetchReference method
CacheRefSummary struct {
Name string
Repo string
Tag string
Exists bool
Manifest *ocispec.Descriptor
Config *ocispec.Descriptor
ContentLayer *ocispec.Descriptor
Size int64
Digest digest.Digest
CreatedAt time.Time
Chart *chart.Chart
}
)
// NewCache returns a new OCI Layout-compliant cache with config
func NewCache(opts ...CacheOption) (*Cache, error) {
cache := &Cache{
out: ioutil.Discard,
}
for _, opt := range opts {
opt(cache)
}
// validate
if cache.rootDir == "" {
return nil, errors.New("must set cache root dir on initialization")
}
return cache, nil
}
// FetchReference retrieves a chart ref from cache
func (cache *Cache) FetchReference(ref *Reference) (*CacheRefSummary, error) {
if err := cache.init(); err != nil {
return nil, err
}
r := CacheRefSummary{
Name: ref.FullName(),
Repo: ref.Repo,
Tag: ref.Tag,
}
for _, desc := range cache.ociStore.ListReferences() {
if desc.Annotations[ocispec.AnnotationRefName] == r.Name {
r.Exists = true
manifestBytes, err := cache.fetchBlob(&desc)
if err != nil {
return &r, err
}
var manifest ocispec.Manifest
err = json.Unmarshal(manifestBytes, &manifest)
if err != nil {
return &r, err
}
r.Manifest = &desc
r.Config = &manifest.Config
numLayers := len(manifest.Layers)
if numLayers != 1 {
return &r, errors.New(
fmt.Sprintf("manifest does not contain exactly 1 layer (total: %d)", numLayers))
}
var contentLayer *ocispec.Descriptor
for _, layer := range manifest.Layers {
switch layer.MediaType {
case HelmChartContentLayerMediaType:
contentLayer = &layer
}
}
if contentLayer == nil {
return &r, errors.New(
fmt.Sprintf("manifest does not contain a layer with mediatype %s", HelmChartContentLayerMediaType))
}
if contentLayer.Size == 0 {
return &r, errors.New(
fmt.Sprintf("manifest layer with mediatype %s is of size 0", HelmChartContentLayerMediaType))
}
r.ContentLayer = contentLayer
info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
if err != nil {
return &r, err
}
r.Size = info.Size
r.Digest = info.Digest
r.CreatedAt = info.CreatedAt
contentBytes, err := cache.fetchBlob(contentLayer)
if err != nil {
return &r, err
}
ch, err := loader.LoadArchive(bytes.NewBuffer(contentBytes))
if err != nil {
return &r, err
}
r.Chart = ch
}
}
return &r, nil
}
// StoreReference stores a chart ref in cache
func (cache *Cache) StoreReference(ref *Reference, ch *chart.Chart) (*CacheRefSummary, error) {
if err := cache.init(); err != nil {
return nil, err
}
r := CacheRefSummary{
Name: ref.FullName(),
Repo: ref.Repo,
Tag: ref.Tag,
Chart: ch,
}
existing, _ := cache.FetchReference(ref)
r.Exists = existing.Exists
config, _, err := cache.saveChartConfig(ch)
if err != nil {
return &r, err
}
r.Config = config
contentLayer, _, err := cache.saveChartContentLayer(ch)
if err != nil {
return &r, err
}
r.ContentLayer = contentLayer
info, err := cache.ociStore.Info(ctx(cache.out, cache.debug), contentLayer.Digest)
if err != nil {
return &r, err
}
r.Size = info.Size
r.Digest = info.Digest
r.CreatedAt = info.CreatedAt
manifest, _, err := cache.saveChartManifest(config, contentLayer)
if err != nil {
return &r, err
}
r.Manifest = manifest
return &r, nil
}
// DeleteReference deletes a chart ref from cache
// TODO: garbage collection, only manifest removed
func (cache *Cache) DeleteReference(ref *Reference) (*CacheRefSummary, error) {
if err := cache.init(); err != nil {
return nil, err
}
r, err := cache.FetchReference(ref)
if err != nil || !r.Exists {
return r, err
}
cache.ociStore.DeleteReference(r.Name)
err = cache.ociStore.SaveIndex()
return r, err
}
// ListReferences lists all chart refs in a cache
func (cache *Cache) ListReferences() ([]*CacheRefSummary, error) {
if err := cache.init(); err != nil {
return nil, err
}
var rr []*CacheRefSummary
for _, desc := range cache.ociStore.ListReferences() {
name := desc.Annotations[ocispec.AnnotationRefName]
if name == "" {
if cache.debug {
fmt.Fprintf(cache.out, "warning: found manifest without name: %s", desc.Digest.Hex())
}
continue
}
ref, err := ParseReference(name)
if err != nil {
return rr, err
}
r, err := cache.FetchReference(ref)
if err != nil {
return rr, err
}
rr = append(rr, r)
}
return rr, nil
}
// AddManifest provides a manifest to the cache index.json
func (cache *Cache) AddManifest(ref *Reference, manifest *ocispec.Descriptor) error {
if err := cache.init(); err != nil {
return err
}
cache.ociStore.AddReference(ref.FullName(), *manifest)
err := cache.ociStore.SaveIndex()
return err
}
// Provider provides a valid containerd Provider
func (cache *Cache) Provider() content.Provider {
return content.Provider(cache.ociStore)
}
// Ingester provides a valid containerd Ingester
func (cache *Cache) Ingester() content.Ingester {
return content.Ingester(cache.ociStore)
}
// ProvideIngester provides a valid oras ProvideIngester
func (cache *Cache) ProvideIngester() orascontent.ProvideIngester {
return orascontent.ProvideIngester(cache.ociStore)
}
// init creates files needed necessary for OCI layout store
func (cache *Cache) init() error {
if cache.ociStore == nil {
ociStore, err := orascontent.NewOCIStore(cache.rootDir)
if err != nil {
return err
}
cache.ociStore = ociStore
cache.memoryStore = orascontent.NewMemoryStore()
}
return nil
}
// saveChartConfig stores the Chart.yaml as json blob and returns a descriptor
func (cache *Cache) saveChartConfig(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
configBytes, err := json.Marshal(ch.Metadata)
if err != nil {
return nil, false, err
}
configExists, err := cache.storeBlob(configBytes)
if err != nil {
return nil, configExists, err
}
descriptor := cache.memoryStore.Add("", HelmChartConfigMediaType, configBytes)
return &descriptor, configExists, nil
}
// saveChartContentLayer stores the chart as tarball blob and returns a descriptor
func (cache *Cache) saveChartContentLayer(ch *chart.Chart) (*ocispec.Descriptor, bool, error) {
destDir := filepath.Join(cache.rootDir, ".build")
os.MkdirAll(destDir, 0755)
tmpFile, err := chartutil.Save(ch, destDir)
defer os.Remove(tmpFile)
if err != nil {
return nil, false, errors.Wrap(err, "failed to save")
}
contentBytes, err := ioutil.ReadFile(tmpFile)
if err != nil {
return nil, false, err
}
contentExists, err := cache.storeBlob(contentBytes)
if err != nil {
return nil, contentExists, err
}
descriptor := cache.memoryStore.Add("", HelmChartContentLayerMediaType, contentBytes)
return &descriptor, contentExists, nil
}
// saveChartManifest stores the chart manifest as json blob and returns a descriptor
func (cache *Cache) saveChartManifest(config *ocispec.Descriptor, contentLayer *ocispec.Descriptor) (*ocispec.Descriptor, bool, error) {
manifest := ocispec.Manifest{
Versioned: specs.Versioned{SchemaVersion: 2},
Config: *config,
Layers: []ocispec.Descriptor{*contentLayer},
}
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return nil, false, err
}
manifestExists, err := cache.storeBlob(manifestBytes)
if err != nil {
return nil, manifestExists, err
}
descriptor := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifestBytes),
Size: int64(len(manifestBytes)),
}
return &descriptor, manifestExists, nil
}
// storeBlob stores a blob on filesystem
func (cache *Cache) storeBlob(blobBytes []byte) (bool, error) {
var exists bool
writer, err := cache.ociStore.Store.Writer(ctx(cache.out, cache.debug),
content.WithRef(digest.FromBytes(blobBytes).Hex()))
if err != nil {
return exists, err
}
_, err = writer.Write(blobBytes)
if err != nil {
return exists, err
}
err = writer.Commit(ctx(cache.out, cache.debug), 0, writer.Digest())
if err != nil {
if !errdefs.IsAlreadyExists(err) {
return exists, err
}
exists = true
}
err = writer.Close()
return exists, err
}
// fetchBlob retrieves a blob from filesystem
func (cache *Cache) fetchBlob(desc *ocispec.Descriptor) ([]byte, error) {
reader, err := cache.ociStore.ReaderAt(ctx(cache.out, cache.debug), *desc)
if err != nil {
return nil, err
}
defer reader.Close()
bytes := make([]byte, desc.Size)
_, err = reader.ReadAt(bytes, 0)
if err != nil {
return nil, err
}
return bytes, nil
}

@ -1,48 +0,0 @@
/*
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/v3/internal/experimental/registry"
import (
"io"
)
type (
// CacheOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default cache
CacheOption func(*Cache)
)
// CacheOptDebug returns a function that sets the debug setting on cache options set
func CacheOptDebug(debug bool) CacheOption {
return func(cache *Cache) {
cache.debug = debug
}
}
// CacheOptWriter returns a function that sets the writer setting on cache options set
func CacheOptWriter(out io.Writer) CacheOption {
return func(cache *Cache) {
cache.out = out
}
}
// CacheOptRoot returns a function that sets the root directory setting on cache options set
func CacheOptRoot(rootDir string) CacheOption {
return func(cache *Cache) {
cache.rootDir = rootDir
}
}

@ -17,325 +17,458 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"sort"
"strings"
"github.com/gosuri/uitable"
"github.com/containerd/containerd/remotes"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
auth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/auth"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"oras.land/oras-go/pkg/content"
"oras.land/oras-go/pkg/oras"
"helm.sh/helm/v3/internal/version"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/helmpath"
)
const (
// CredentialsFileBasename is the filename for auth credentials file
CredentialsFileBasename = "config.json"
)
type (
// Client works with OCI-compliant registries and local Helm chart cache
// Client works with OCI-compliant registries
Client struct {
debug bool
// path to repository config file e.g. ~/.docker/config.json
credentialsFile string
out io.Writer
authorizer *Authorizer
resolver *Resolver
cache *Cache
columnWidth uint
authorizer auth.Client
resolver remotes.Resolver
}
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default client
ClientOption func(*Client)
)
// NewClient returns a new registry client with config
func NewClient(opts ...ClientOption) (*Client, error) {
func NewClient(options ...ClientOption) (*Client, error) {
client := &Client{
out: ioutil.Discard,
}
for _, opt := range opts {
opt(client)
for _, option := range options {
option(client)
}
// set defaults if fields are missing
if client.credentialsFile == "" {
client.credentialsFile = helmpath.CachePath("registry", CredentialsFileBasename)
}
if client.authorizer == nil {
authClient, err := auth.NewClient(client.credentialsFile)
authClient, err := dockerauth.NewClient(client.credentialsFile)
if err != nil {
return nil, err
}
client.authorizer = &Authorizer{
Client: authClient,
}
client.authorizer = authClient
}
if client.resolver == nil {
resolver, err := client.authorizer.Resolver(context.Background(), http.DefaultClient, false)
headers := http.Header{}
headers.Set("User-Agent", version.GetUserAgent())
opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
resolver, err := client.authorizer.ResolverWithOpts(opts...)
if err != nil {
return nil, err
}
client.resolver = &Resolver{
Resolver: resolver,
}
client.resolver = resolver
}
if client.cache == nil {
cache, err := NewCache(
CacheOptDebug(client.debug),
CacheOptWriter(client.out),
CacheOptRoot(helmpath.CachePath("registry", CacheRootDir)),
)
if err != nil {
return nil, err
}
client.cache = cache
return client, nil
}
// ClientOptDebug returns a function that sets the debug setting on client options set
func ClientOptDebug(debug bool) ClientOption {
return func(client *Client) {
client.debug = debug
}
}
if client.columnWidth == 0 {
client.columnWidth = 60
// ClientOptWriter returns a function that sets the writer setting on client options set
func ClientOptWriter(out io.Writer) ClientOption {
return func(client *Client) {
client.out = out
}
}
// ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set
func ClientOptCredentialsFile(credentialsFile string) ClientOption {
return func(client *Client) {
client.credentialsFile = credentialsFile
}
return client, nil
}
type (
// LoginOption allows specifying various settings on login
LoginOption func(*loginOperation)
loginOperation struct {
username string
password string
insecure bool
}
)
// Login logs into a registry
func (c *Client) Login(hostname string, username string, password string, insecure bool) error {
err := c.authorizer.Login(ctx(c.out, c.debug), hostname, username, password, insecure)
if err != nil {
func (c *Client) Login(host string, options ...LoginOption) error {
operation := &loginOperation{}
for _, option := range options {
option(operation)
}
authorizerLoginOpts := []auth.LoginOption{
auth.WithLoginContext(ctx(c.out, c.debug)),
auth.WithLoginHostname(host),
auth.WithLoginUsername(operation.username),
auth.WithLoginSecret(operation.password),
auth.WithLoginUserAgent(version.GetUserAgent()),
}
if operation.insecure {
authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure())
}
if err := c.authorizer.LoginWithOpts(authorizerLoginOpts...); err != nil {
return err
}
fmt.Fprintf(c.out, "Login succeeded\n")
fmt.Fprintln(c.out, "Login Succeeded")
return nil
}
// Logout logs out of a registry
func (c *Client) Logout(hostname string) error {
err := c.authorizer.Logout(ctx(c.out, c.debug), hostname)
if err != nil {
return err
// LoginOptBasicAuth returns a function that sets the username/password settings on login
func LoginOptBasicAuth(username string, password string) LoginOption {
return func(operation *loginOperation) {
operation.username = username
operation.password = password
}
fmt.Fprintln(c.out, "Logout succeeded")
return nil
}
// PushChart uploads a chart to a registry
func (c *Client) PushChart(ref *Reference) error {
r, err := c.cache.FetchReference(ref)
if err != nil {
return err
// LoginOptInsecure returns a function that sets the insecure setting on login
func LoginOptInsecure(insecure bool) LoginOption {
return func(operation *loginOperation) {
operation.insecure = insecure
}
if !r.Exists {
return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
}
type (
// LogoutOption allows specifying various settings on logout
LogoutOption func(*logoutOperation)
logoutOperation struct{}
)
// Logout logs out of a registry
func (c *Client) Logout(host string, opts ...LogoutOption) error {
operation := &logoutOperation{}
for _, opt := range opts {
opt(operation)
}
fmt.Fprintf(c.out, "The push refers to repository [%s]\n", r.Repo)
c.printCacheRefSummary(r)
layers := []ocispec.Descriptor{*r.ContentLayer}
_, err = oras.Push(ctx(c.out, c.debug), c.resolver, r.Name, c.cache.Provider(), layers,
oras.WithConfig(*r.Config), oras.WithNameValidation(nil))
if err != nil {
if err := c.authorizer.Logout(ctx(c.out, c.debug), host); err != nil {
return err
}
s := ""
numLayers := len(layers)
if 1 < numLayers {
s = "s"
}
fmt.Fprintf(c.out,
"%s: pushed to remote (%d layer%s, %s total)\n", r.Tag, numLayers, s, byteCountBinary(r.Size))
fmt.Fprintf(c.out, "Removing login credentials for %s\n", host)
return nil
}
// PullChart downloads a chart from a registry
func (c *Client) PullChart(ref *Reference) (*bytes.Buffer, error) {
buf := bytes.NewBuffer(nil)
type (
// PullOption allows specifying various settings on pull
PullOption func(*pullOperation)
if ref.Tag == "" {
return buf, errors.New("tag explicitly required")
// PullResult is the result returned upon successful pull.
PullResult struct {
Manifest *descriptorPullSummary `json:"manifest"`
Config *descriptorPullSummary `json:"config"`
Chart *descriptorPullSummaryWithMeta `json:"chart"`
Prov *descriptorPullSummary `json:"prov"`
Ref string `json:"ref"`
}
fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo)
store := content.NewMemoryStore()
fullname := ref.FullName()
_ = fullname
_, layerDescriptors, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), store,
oras.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(KnownMediaTypes()))
if err != nil {
return buf, err
descriptorPullSummary struct {
Data []byte `json:"-"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
numLayers := len(layerDescriptors)
if numLayers < 1 {
return buf, errors.New(
fmt.Sprintf("manifest does not contain at least 1 layer (total: %d)", numLayers))
descriptorPullSummaryWithMeta struct {
descriptorPullSummary
Meta *chart.Metadata `json:"meta"`
}
var contentLayer *ocispec.Descriptor
for _, layer := range layerDescriptors {
layer := layer
switch layer.MediaType {
case HelmChartContentLayerMediaType:
contentLayer = &layer
}
pullOperation struct {
withChart bool
withProv bool
ignoreMissingProv bool
}
)
if contentLayer == nil {
return buf, errors.New(
fmt.Sprintf("manifest does not contain a layer with mediatype %s",
HelmChartContentLayerMediaType))
// Pull downloads a chart from a registry
func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
operation := &pullOperation{
withChart: true, // By default, always download the chart layer
}
_, b, ok := store.Get(*contentLayer)
if !ok {
return buf, errors.Errorf("Unable to retrieve blob with digest %s", contentLayer.Digest)
for _, option := range options {
option(operation)
}
buf = bytes.NewBuffer(b)
return buf, nil
}
// PullChartToCache pulls a chart from an OCI Registry to the Registry Cache.
// This function is needed for `helm chart pull`, which is experimental and will be deprecated soon.
// Likewise, the Registry cache will soon be deprecated as will this function.
func (c *Client) PullChartToCache(ref *Reference) error {
if ref.Tag == "" {
return errors.New("tag explicitly required")
if !operation.withChart && !operation.withProv {
return nil, errors.New(
"must specify at least one layer to pull (chart/prov)")
}
existing, err := c.cache.FetchReference(ref)
if err != nil {
return err
store := content.NewMemoryStore()
allowedMediaTypes := []string{
ConfigMediaType,
}
minNumDescriptors := 1 // 1 for the config
if operation.withChart {
minNumDescriptors++
allowedMediaTypes = append(allowedMediaTypes, ChartLayerMediaType)
}
fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo)
manifest, _, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref.FullName(), c.cache.Ingester(),
if operation.withProv {
if !operation.ignoreMissingProv {
minNumDescriptors++
}
allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType)
}
manifest, descriptors, err := oras.Pull(ctx(c.out, c.debug), c.resolver, ref, store,
oras.WithPullEmptyNameAllowed(),
oras.WithAllowedMediaTypes(KnownMediaTypes()),
oras.WithContentProvideIngester(c.cache.ProvideIngester()))
oras.WithAllowedMediaTypes(allowedMediaTypes))
if err != nil {
return err
return nil, err
}
err = c.cache.AddManifest(ref, &manifest)
if err != nil {
return err
numDescriptors := len(descriptors)
if numDescriptors < minNumDescriptors {
return nil, errors.New(
fmt.Sprintf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d",
minNumDescriptors, numDescriptors))
}
var configDescriptor *ocispec.Descriptor
var chartDescriptor *ocispec.Descriptor
var provDescriptor *ocispec.Descriptor
for _, descriptor := range descriptors {
d := descriptor
switch d.MediaType {
case ConfigMediaType:
configDescriptor = &d
case ChartLayerMediaType:
chartDescriptor = &d
case ProvLayerMediaType:
provDescriptor = &d
}
}
r, err := c.cache.FetchReference(ref)
if err != nil {
return err
if configDescriptor == nil {
return nil, errors.New(
fmt.Sprintf("could not load config with mediatype %s", ConfigMediaType))
}
if !r.Exists {
return errors.New(fmt.Sprintf("Chart not found: %s", r.Name))
if operation.withChart && chartDescriptor == nil {
return nil, errors.New(
fmt.Sprintf("manifest does not contain a layer with mediatype %s",
ChartLayerMediaType))
}
var provMissing bool
if operation.withProv && provDescriptor == nil {
if operation.ignoreMissingProv {
provMissing = true
} else {
return nil, errors.New(
fmt.Sprintf("manifest does not contain a layer with mediatype %s",
ProvLayerMediaType))
}
}
c.printCacheRefSummary(r)
if !existing.Exists {
fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s\n", ref.FullName())
result := &PullResult{
Manifest: &descriptorPullSummary{
Digest: manifest.Digest.String(),
Size: manifest.Size,
},
Config: &descriptorPullSummary{
Digest: configDescriptor.Digest.String(),
Size: configDescriptor.Size,
},
Chart: &descriptorPullSummaryWithMeta{},
Prov: &descriptorPullSummary{},
Ref: ref,
}
var getManifestErr error
if _, manifestData, ok := store.Get(manifest); !ok {
getManifestErr = errors.Errorf("Unable to retrieve blob with digest %s", manifest.Digest)
} else {
fmt.Fprintf(c.out, "Status: Chart is up to date for %s\n", ref.FullName())
result.Manifest.Data = manifestData
}
return err
}
// SaveChart stores a copy of chart in local cache
func (c *Client) SaveChart(ch *chart.Chart, ref *Reference) error {
r, err := c.cache.StoreReference(ref, ch)
if err != nil {
return err
if getManifestErr != nil {
return nil, getManifestErr
}
c.printCacheRefSummary(r)
err = c.cache.AddManifest(ref, r.Manifest)
if err != nil {
return err
var getConfigDescriptorErr error
if _, configData, ok := store.Get(*configDescriptor); !ok {
getConfigDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", configDescriptor.Digest)
} else {
result.Config.Data = configData
var meta *chart.Metadata
if err := json.Unmarshal(configData, &meta); err != nil {
return nil, err
}
result.Chart.Meta = meta
}
if getConfigDescriptorErr != nil {
return nil, getConfigDescriptorErr
}
if operation.withChart {
var getChartDescriptorErr error
if _, chartData, ok := store.Get(*chartDescriptor); !ok {
getChartDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", chartDescriptor.Digest)
} else {
result.Chart.Data = chartData
result.Chart.Digest = chartDescriptor.Digest.String()
result.Chart.Size = chartDescriptor.Size
}
if getChartDescriptorErr != nil {
return nil, getChartDescriptorErr
}
}
fmt.Fprintf(c.out, "%s: saved\n", r.Tag)
return nil
if operation.withProv && !provMissing {
var getProvDescriptorErr error
if _, provData, ok := store.Get(*provDescriptor); !ok {
getProvDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", provDescriptor.Digest)
} else {
result.Prov.Data = provData
result.Prov.Digest = provDescriptor.Digest.String()
result.Prov.Size = provDescriptor.Size
}
if getProvDescriptorErr != nil {
return nil, getProvDescriptorErr
}
}
fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref)
fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
return result, nil
}
// LoadChart retrieves a chart object by reference
func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) {
r, err := c.cache.FetchReference(ref)
if err != nil {
return nil, err
// PullOptWithChart returns a function that sets the withChart setting on pull
func PullOptWithChart(withChart bool) PullOption {
return func(operation *pullOperation) {
operation.withChart = withChart
}
if !r.Exists {
return nil, errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName()))
}
c.printCacheRefSummary(r)
return r.Chart, nil
}
// RemoveChart deletes a locally saved chart
func (c *Client) RemoveChart(ref *Reference) error {
r, err := c.cache.DeleteReference(ref)
if err != nil {
return err
// PullOptWithProv returns a function that sets the withProv setting on pull
func PullOptWithProv(withProv bool) PullOption {
return func(operation *pullOperation) {
operation.withProv = withProv
}
if !r.Exists {
return errors.New(fmt.Sprintf("Chart not found: %s", ref.FullName()))
}
// PullOptIgnoreMissingProv returns a function that sets the ignoreMissingProv setting on pull
func PullOptIgnoreMissingProv(ignoreMissingProv bool) PullOption {
return func(operation *pullOperation) {
operation.ignoreMissingProv = ignoreMissingProv
}
fmt.Fprintf(c.out, "%s: removed\n", r.Tag)
return nil
}
// PrintChartTable prints a list of locally stored charts
func (c *Client) PrintChartTable() error {
table := uitable.New()
table.MaxColWidth = c.columnWidth
table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED")
rows, err := c.getChartTableRows()
if err != nil {
return err
type (
// PushOption allows specifying various settings on push
PushOption func(*pushOperation)
// PushResult is the result returned upon successful push.
PushResult struct {
Manifest *descriptorPushSummary `json:"manifest"`
Config *descriptorPushSummary `json:"config"`
Chart *descriptorPushSummaryWithMeta `json:"chart"`
Prov *descriptorPushSummary `json:"prov"`
Ref string `json:"ref"`
}
for _, row := range rows {
table.AddRow(row...)
descriptorPushSummary struct {
Digest string `json:"digest"`
Size int64 `json:"size"`
}
fmt.Fprintln(c.out, table.String())
return nil
}
// printCacheRefSummary prints out chart ref summary
func (c *Client) printCacheRefSummary(r *CacheRefSummary) {
fmt.Fprintf(c.out, "ref: %s\n", r.Name)
fmt.Fprintf(c.out, "digest: %s\n", r.Manifest.Digest.Hex())
fmt.Fprintf(c.out, "size: %s\n", byteCountBinary(r.Size))
fmt.Fprintf(c.out, "name: %s\n", r.Chart.Metadata.Name)
fmt.Fprintf(c.out, "version: %s\n", r.Chart.Metadata.Version)
}
descriptorPushSummaryWithMeta struct {
descriptorPushSummary
Meta *chart.Metadata `json:"meta"`
}
// getChartTableRows returns rows in uitable-friendly format
func (c *Client) getChartTableRows() ([][]interface{}, error) {
rr, err := c.cache.ListReferences()
pushOperation struct {
provData []byte
strictMode bool
}
)
// Push uploads a chart to a registry.
func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) {
operation := &pushOperation{
strictMode: true, // By default, enable strict mode
}
for _, option := range options {
option(operation)
}
meta, err := extractChartMeta(data)
if err != nil {
return nil, err
}
refsMap := map[string]map[string]string{}
for _, r := range rr {
refsMap[r.Name] = map[string]string{
"name": r.Chart.Metadata.Name,
"version": r.Chart.Metadata.Version,
"digest": shortDigest(r.Manifest.Digest.Hex()),
"size": byteCountBinary(r.Size),
"created": timeAgo(r.CreatedAt),
if operation.strictMode {
if !strings.HasSuffix(ref, fmt.Sprintf("/%s:%s", meta.Name, meta.Version)) {
return nil, errors.New(
"strict mode enabled, ref basename and tag must match the chart name and version")
}
}
// Sort and convert to format expected by uitable
rows := make([][]interface{}, len(refsMap))
keys := make([]string, 0, len(refsMap))
for key := range refsMap {
keys = append(keys, key)
}
sort.Strings(keys)
for i, key := range keys {
rows[i] = make([]interface{}, 6)
rows[i][0] = key
ref := refsMap[key]
for j, k := range []string{"name", "version", "digest", "size", "created"} {
rows[i][j+1] = ref[k]
store := content.NewMemoryStore()
chartDescriptor := store.Add("", ChartLayerMediaType, data)
configData, err := json.Marshal(meta)
if err != nil {
return nil, err
}
configDescriptor := store.Add("", ConfigMediaType, configData)
descriptors := []ocispec.Descriptor{chartDescriptor}
var provDescriptor ocispec.Descriptor
if operation.provData != nil {
provDescriptor = store.Add("", ProvLayerMediaType, operation.provData)
descriptors = append(descriptors, provDescriptor)
}
manifest, err := oras.Push(ctx(c.out, c.debug), c.resolver, ref, store, descriptors,
oras.WithConfig(configDescriptor), oras.WithNameValidation(nil))
if err != nil {
return nil, err
}
chartSummary := &descriptorPushSummaryWithMeta{
Meta: meta,
}
chartSummary.Digest = chartDescriptor.Digest.String()
chartSummary.Size = chartDescriptor.Size
result := &PushResult{
Manifest: &descriptorPushSummary{
Digest: manifest.Digest.String(),
Size: manifest.Size,
},
Config: &descriptorPushSummary{
Digest: configDescriptor.Digest.String(),
Size: configDescriptor.Size,
},
Chart: chartSummary,
Prov: &descriptorPushSummary{}, // prevent nil references
Ref: ref,
}
if operation.provData != nil {
result.Prov = &descriptorPushSummary{
Digest: provDescriptor.Digest.String(),
Size: provDescriptor.Size,
}
}
return rows, nil
fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref)
fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest)
return result, err
}
// PushOptProvData returns a function that sets the prov bytes setting on push
func PushOptProvData(provData []byte) PushOption {
return func(operation *pushOperation) {
operation.provData = provData
}
}
// PushOptStrictMode returns a function that sets the strictMode setting on push
func PushOptStrictMode(strictMode bool) PushOption {
return func(operation *pushOperation) {
operation.strictMode = strictMode
}
}

@ -1,76 +0,0 @@
/*
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/v3/internal/experimental/registry"
import (
"io"
)
type (
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
// used when creating a new default client
ClientOption func(*Client)
)
// ClientOptDebug returns a function that sets the debug setting on client options set
func ClientOptDebug(debug bool) ClientOption {
return func(client *Client) {
client.debug = debug
}
}
// ClientOptWriter returns a function that sets the writer setting on client options set
func ClientOptWriter(out io.Writer) ClientOption {
return func(client *Client) {
client.out = out
}
}
// ClientOptResolver returns a function that sets the resolver setting on client options set
func ClientOptResolver(resolver *Resolver) ClientOption {
return func(client *Client) {
client.resolver = resolver
}
}
// ClientOptAuthorizer returns a function that sets the authorizer setting on client options set
func ClientOptAuthorizer(authorizer *Authorizer) ClientOption {
return func(client *Client) {
client.authorizer = authorizer
}
}
// ClientOptCache returns a function that sets the cache setting on a client options set
func ClientOptCache(cache *Cache) ClientOption {
return func(client *Client) {
client.cache = cache
}
}
// ClientOptCredentialsFile returns a function that sets the cache setting on a client options set
func ClientOptCredentialsFile(credentialsFile string) ClientOption {
return func(client *Client) {
client.credentialsFile = credentialsFile
}
}
// ClientOptColumnWidth returns a function that sets the column width on a client options set
func ClientOptColumnWidth(columnWidth uint) ClientOption {
return func(client *Client) {
client.columnWidth = columnWidth
}
}

@ -39,13 +39,10 @@ import (
"github.com/phayes/freeport"
"github.com/stretchr/testify/suite"
"golang.org/x/crypto/bcrypt"
auth "oras.land/oras-go/pkg/auth/docker"
"helm.sh/helm/v3/pkg/chart"
)
var (
testCacheRootDir = "helm-registry-test"
testWorkspaceDir = "helm-registry-test"
testHtpasswdFileBasename = "authtest.htpasswd"
testUsername = "myuser"
testPassword = "mypass"
@ -56,51 +53,32 @@ type RegistryClientTestSuite struct {
Out io.Writer
DockerRegistryHost string
CompromisedRegistryHost string
CacheRootDir string
WorkspaceDir string
RegistryClient *Client
}
func (suite *RegistryClientTestSuite) SetupSuite() {
suite.CacheRootDir = testCacheRootDir
os.RemoveAll(suite.CacheRootDir)
os.Mkdir(suite.CacheRootDir, 0700)
suite.WorkspaceDir = testWorkspaceDir
os.RemoveAll(suite.WorkspaceDir)
os.Mkdir(suite.WorkspaceDir, 0700)
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(), http.DefaultClient, false)
suite.Nil(err, "no error creating resolver")
// create cache
cache, err := NewCache(
CacheOptDebug(true),
CacheOptWriter(suite.Out),
CacheOptRoot(filepath.Join(suite.CacheRootDir, CacheRootDir)),
)
suite.Nil(err, "no error creating cache")
credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename)
// init test client
var err error
suite.RegistryClient, err = NewClient(
ClientOptDebug(true),
ClientOptWriter(suite.Out),
ClientOptAuthorizer(&Authorizer{
Client: client,
}),
ClientOptResolver(&Resolver{
Resolver: resolver,
}),
ClientOptCache(cache),
ClientOptCredentialsFile(credentialsFile),
)
suite.Nil(err, "no error creating registry client")
// 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)
htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename)
err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
suite.Nil(err, "no error creating test htpasswd file")
@ -109,7 +87,7 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
port, err := freeport.GetFreePort()
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.Addr = fmt.Sprintf("127.0.0.1:%d", port)
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
config.Auth = configuration.Auth{
@ -128,110 +106,195 @@ func (suite *RegistryClientTestSuite) SetupSuite() {
}
func (suite *RegistryClientTestSuite) TearDownSuite() {
os.RemoveAll(suite.CacheRootDir)
os.RemoveAll(suite.WorkspaceDir)
}
func (suite *RegistryClientTestSuite) Test_0_Login() {
err := suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", false)
err := suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth("badverybad", "ohsobad"),
LoginOptInsecure(false))
suite.NotNil(err, "error logging into registry with bad credentials")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, "badverybad", "ohsobad", true)
err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth("badverybad", "ohsobad"),
LoginOptInsecure(true))
suite.NotNil(err, "error logging into registry with bad credentials, insecure mode")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, false)
err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth(testUsername, testPassword),
LoginOptInsecure(false))
suite.Nil(err, "no error logging into registry with good credentials")
err = suite.RegistryClient.Login(suite.DockerRegistryHost, testUsername, testPassword, true)
err = suite.RegistryClient.Login(suite.DockerRegistryHost,
LoginOptBasicAuth(testUsername, testPassword),
LoginOptInsecure(true))
suite.Nil(err, "no error logging into registry with good credentials, insecure mode")
}
func (suite *RegistryClientTestSuite) Test_1_SaveChart() {
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
suite.Nil(err)
// empty chart
err = suite.RegistryClient.SaveChart(&chart.Chart{}, ref)
suite.NotNil(err)
// valid chart
ch := &chart.Chart{}
ch.Metadata = &chart.Metadata{
APIVersion: "v1",
Name: "testchart",
Version: "1.2.3",
}
err = suite.RegistryClient.SaveChart(ch, ref)
suite.Nil(err)
func (suite *RegistryClientTestSuite) Test_1_Push() {
// Bad bytes
ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)
_, err := suite.RegistryClient.Push([]byte("hello"), ref)
suite.NotNil(err, "error pushing non-chart bytes")
// Load a test chart
chartData, err := ioutil.ReadFile("../../../pkg/repo/repotest/testdata/examplechart-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err := extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
// non-strict ref (chart name)
ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version)
_, err = suite.RegistryClient.Push(chartData, ref)
suite.NotNil(err, "error pushing non-strict ref (bad basename)")
// non-strict ref (chart name), with strict mode disabled
_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled")
// non-strict ref (chart version)
ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name)
_, err = suite.RegistryClient.Push(chartData, ref)
suite.NotNil(err, "error pushing non-strict ref (bad tag)")
// non-strict ref (chart version), with strict mode disabled
_, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false))
suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled")
// basic push, good ref
chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/local-subchart-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
_, err = suite.RegistryClient.Push(chartData, ref)
suite.Nil(err, "no error pushing good ref")
_, err = suite.RegistryClient.Pull(ref)
suite.Nil(err, "no error pulling a simple chart")
// Load another test chart
chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
// Load prov file
provData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz.prov")
suite.Nil(err, "no error loading test prov")
// push with prov
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData))
suite.Nil(err, "no error pushing good ref with prov")
_, err = suite.RegistryClient.Pull(ref)
suite.Nil(err, "no error pulling a simple chart")
// Validate the output
// Note: these digests/sizes etc may change if the test chart/prov files are modified,
// or if the format of the OCI manifest changes
suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name)
suite.Equal(meta.Version, result.Chart.Meta.Version)
suite.Equal(int64(512), result.Manifest.Size)
suite.Equal(int64(99), result.Config.Size)
suite.Equal(int64(973), result.Chart.Size)
suite.Equal(int64(695), result.Prov.Size)
suite.Equal(
"sha256:c4fd4ca31f12f50a7f704bb1dfdf2e768b1e8bdeac3991b534b6bdb3f535aab1",
result.Manifest.Digest)
suite.Equal(
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
result.Config.Digest)
suite.Equal(
"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
result.Chart.Digest)
suite.Equal(
"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
result.Prov.Digest)
}
func (suite *RegistryClientTestSuite) Test_2_LoadChart() {
// non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
suite.Nil(err)
_, err = suite.RegistryClient.LoadChart(ref)
suite.NotNil(err)
// existing ref
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
suite.Nil(err)
ch, err := suite.RegistryClient.LoadChart(ref)
suite.Nil(err)
suite.Equal("testchart", ch.Metadata.Name)
suite.Equal("1.2.3", ch.Metadata.Version)
}
func (suite *RegistryClientTestSuite) Test_3_PushChart() {
// non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
suite.Nil(err)
err = suite.RegistryClient.PushChart(ref)
suite.NotNil(err)
// existing ref
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
suite.Nil(err)
err = suite.RegistryClient.PushChart(ref)
suite.Nil(err)
}
func (suite *RegistryClientTestSuite) Test_4_PullChart() {
// non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
suite.Nil(err)
_, err = suite.RegistryClient.PullChart(ref)
suite.NotNil(err)
// existing ref
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
suite.Nil(err)
_, err = suite.RegistryClient.PullChart(ref)
suite.Nil(err)
}
func (suite *RegistryClientTestSuite) Test_5_PrintChartTable() {
err := suite.RegistryClient.PrintChartTable()
suite.Nil(err)
}
func (suite *RegistryClientTestSuite) Test_6_RemoveChart() {
// non-existent ref
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost))
suite.Nil(err)
err = suite.RegistryClient.RemoveChart(ref)
suite.NotNil(err)
// existing ref
ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost))
suite.Nil(err)
err = suite.RegistryClient.RemoveChart(ref)
suite.Nil(err)
func (suite *RegistryClientTestSuite) Test_2_Pull() {
// bad/missing ref
ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost)
_, err := suite.RegistryClient.Pull(ref)
suite.NotNil(err, "error on bad/missing ref")
// Load test chart (to build ref pushed in previous test)
chartData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/local-subchart-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err := extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
// Simple pull, chart only
_, err = suite.RegistryClient.Pull(ref)
suite.Nil(err, "no error pulling a simple chart")
// Simple pull with prov (no prov uploaded)
_, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true))
suite.NotNil(err, "error pulling a chart with prov when no prov exists")
// Simple pull with prov, ignoring missing prov
_, err = suite.RegistryClient.Pull(ref,
PullOptWithProv(true),
PullOptIgnoreMissingProv(true))
suite.Nil(err,
"no error pulling a chart with prov when no prov exists, ignoring missing")
// Load test chart (to build ref pushed in previous test)
chartData, err = ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz")
suite.Nil(err, "no error loading test chart")
meta, err = extractChartMeta(chartData)
suite.Nil(err, "no error extracting chart meta")
ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version)
// Load prov file
provData, err := ioutil.ReadFile("../../../pkg/downloader/testdata/signtest-0.1.0.tgz.prov")
suite.Nil(err, "no error loading test prov")
// no chart and no prov causes error
_, err = suite.RegistryClient.Pull(ref,
PullOptWithChart(false),
PullOptWithProv(false))
suite.NotNil(err, "error on both no chart and no prov")
// full pull with chart and prov
result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true))
suite.Nil(err, "no error pulling a chart with prov")
// Validate the output
// Note: these digests/sizes etc may change if the test chart/prov files are modified,
// or if the format of the OCI manifest changes
suite.Equal(ref, result.Ref)
suite.Equal(meta.Name, result.Chart.Meta.Name)
suite.Equal(meta.Version, result.Chart.Meta.Version)
suite.Equal(int64(512), result.Manifest.Size)
suite.Equal(int64(99), result.Config.Size)
suite.Equal(int64(973), result.Chart.Size)
suite.Equal(int64(695), result.Prov.Size)
suite.Equal(
"sha256:c4fd4ca31f12f50a7f704bb1dfdf2e768b1e8bdeac3991b534b6bdb3f535aab1",
result.Manifest.Digest)
suite.Equal(
"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580",
result.Config.Digest)
suite.Equal(
"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
result.Chart.Digest)
suite.Equal(
"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256",
result.Prov.Digest)
suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973},{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695}]}",
string(result.Manifest.Data))
suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}",
string(result.Config.Data))
suite.Equal(chartData, result.Chart.Data)
suite.Equal(provData, result.Prov.Data)
}
func (suite *RegistryClientTestSuite) Test_7_Logout() {
func (suite *RegistryClientTestSuite) Test_3_Logout() {
err := suite.RegistryClient.Logout("this-host-aint-real:5000")
suite.NotNil(err, "error logging out of registry that has no entry")
@ -239,12 +302,11 @@ func (suite *RegistryClientTestSuite) Test_7_Logout() {
suite.Nil(err, "no error logging out of registry")
}
func (suite *RegistryClientTestSuite) Test_8_ManInTheMiddle() {
ref, err := ParseReference(fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost))
suite.Nil(err)
func (suite *RegistryClientTestSuite) Test_4_ManInTheMiddle() {
ref := fmt.Sprintf("%s/testrepo/supposedlysafechart:9.9.9", suite.CompromisedRegistryHost)
// returns content that does not match the expected digest
_, err = suite.RegistryClient.PullChart(ref)
_, err := suite.RegistryClient.Pull(ref)
suite.NotNil(err)
suite.True(errdefs.IsFailedPrecondition(err))
}
@ -261,19 +323,19 @@ func initCompromisedRegistryTestServer() string {
// layers[0] is the blob []byte("a")
w.Write([]byte(
`{ "schemaVersion": 2, "config": {
"mediaType": "application/vnd.cncf.helm.config.v1+json",
fmt.Sprintf(`{ "schemaVersion": 2, "config": {
"mediaType": "%s",
"digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133",
"size": 181
},
"layers": [
{
"mediaType": "application/tar+gzip",
"mediaType": "%s",
"digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb",
"size": 1
}
]
}`))
}`, ConfigMediaType, ChartLayerMediaType)))
} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
@ -281,7 +343,7 @@ func initCompromisedRegistryTestServer() string {
"an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" +
"\"application\"}"))
} else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" {
w.Header().Set("Content-Type", "application/tar+gzip")
w.Header().Set("Content-Type", ChartLayerMediaType)
w.WriteHeader(200)
w.Write([]byte("b"))
} else {

@ -17,17 +17,18 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
const (
// HelmChartConfigMediaType is the reserved media type for the Helm chart manifest config
HelmChartConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
// OCIScheme is the URL scheme for OCI-based requests
OCIScheme = "oci"
// HelmChartContentLayerMediaType is the reserved media type for Helm chart package content
HelmChartContentLayerMediaType = "application/tar+gzip"
)
// CredentialsFileBasename is the filename for auth credentials file
CredentialsFileBasename = "config.json"
// ConfigMediaType is the reserved media type for the Helm chart manifest config
ConfigMediaType = "application/vnd.cncf.helm.config.v1+json"
// KnownMediaTypes returns a list of layer mediaTypes that the Helm client knows about
func KnownMediaTypes() []string {
return []string{
HelmChartConfigMediaType,
HelmChartContentLayerMediaType,
}
}
// ChartLayerMediaType is the reserved media type for Helm chart package content
ChartLayerMediaType = "application/vnd.cncf.helm.chart.content.v1.tar+gzip"
// ProvLayerMediaType is the reserved media type for Helm chart provenance files
ProvLayerMediaType = "application/vnd.cncf.helm.chart.provenance.v1.prov"
)

@ -1,146 +0,0 @@
/*
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/v3/internal/experimental/registry"
import (
"errors"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
)
var (
validPortRegEx = regexp.MustCompile(`^([1-9]\d{0,3}|0|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$`) // adapted from https://stackoverflow.com/a/12968117
// TODO: Currently we don't support digests, so we are only splitting on the
// colon. However, when we add support for digests, we'll need to use the
// regexp anyway to split on both colons and @, so leaving it like this for
// now
referenceDelimiter = regexp.MustCompile(`[:]`)
errEmptyRepo = errors.New("parsed repo was empty")
errTooManyColons = errors.New("ref may only contain a single colon character (:) unless specifying a port number")
)
type (
// Reference defines the main components of a reference specification
Reference struct {
Tag string
Repo string
}
)
// ParseReference converts a string to a Reference
func ParseReference(s string) (*Reference, error) {
if s == "" {
return nil, errEmptyRepo
}
// Split the components of the string on the colon or @, if it is more than 3,
// immediately return an error. Other validation will be performed later in
// the function
splitComponents := fixSplitComponents(referenceDelimiter.Split(s, -1))
if len(splitComponents) > 3 {
return nil, errTooManyColons
}
var ref *Reference
switch len(splitComponents) {
case 1:
ref = &Reference{Repo: splitComponents[0]}
case 2:
ref = &Reference{Repo: splitComponents[0], Tag: splitComponents[1]}
case 3:
ref = &Reference{Repo: strings.Join(splitComponents[:2], ":"), Tag: splitComponents[2]}
}
// ensure the reference is valid
err := ref.validate()
if err != nil {
return nil, err
}
return ref, nil
}
// FullName the full name of a reference (repo:tag)
func (ref *Reference) FullName() string {
if ref.Tag == "" {
return ref.Repo
}
return fmt.Sprintf("%s:%s", ref.Repo, ref.Tag)
}
// validate makes sure the ref meets our criteria
func (ref *Reference) validate() error {
err := ref.validateRepo()
if err != nil {
return err
}
return ref.validateNumColons()
}
// validateRepo checks that the Repo field is non-empty
func (ref *Reference) validateRepo() error {
if ref.Repo == "" {
return errEmptyRepo
}
// Makes sure the repo results in a parsable URL (similar to what is done
// with containerd reference parsing)
_, err := url.Parse("//" + ref.Repo)
return err
}
// validateNumColons ensures the ref only contains a single colon character (:)
// (or potentially two, there might be a port number specified i.e. :5000)
func (ref *Reference) validateNumColons() error {
if strings.Contains(ref.Tag, ":") {
return errTooManyColons
}
parts := strings.Split(ref.Repo, ":")
lastIndex := len(parts) - 1
if 1 < lastIndex {
return errTooManyColons
}
if 0 < lastIndex {
port := strings.Split(parts[lastIndex], "/")[0]
if !isValidPort(port) {
return errTooManyColons
}
}
return nil
}
// isValidPort returns whether or not a string looks like a valid port
func isValidPort(s string) bool {
return validPortRegEx.MatchString(s)
}
// fixSplitComponents this will modify reference parts based on presence of port
// Example: {localhost, 5000/x/y/z, 0.1.0} => {localhost:5000/x/y/z, 0.1.0}
func fixSplitComponents(c []string) []string {
if len(c) <= 1 {
return c
}
possiblePortParts := strings.Split(c[1], "/")
if _, err := strconv.Atoi(possiblePortParts[0]); err == nil {
components := []string{strings.Join(c[:2], ":")}
components = append(components, c[2:]...)
return components
}
return c
}

@ -1,126 +0,0 @@
/*
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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseReference(t *testing.T) {
is := assert.New(t)
// bad refs
s := ""
_, err := ParseReference(s)
is.Error(err, "empty ref")
s = "my:bad:ref"
_, err = ParseReference(s)
is.Error(err, "ref contains too many colons (2)")
s = "my:really:bad:ref"
_, err = ParseReference(s)
is.Error(err, "ref contains too many colons (3)")
// good refs
s = "mychart"
ref, err := ParseReference(s)
is.NoError(err)
is.Equal("mychart", ref.Repo)
is.Equal("", ref.Tag)
is.Equal("mychart", ref.FullName())
s = "mychart:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("mychart:1.5.0", ref.FullName())
s = "myrepo/mychart"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("myrepo/mychart", ref.Repo)
is.Equal("", ref.Tag)
is.Equal("myrepo/mychart", ref.FullName())
s = "myrepo/mychart:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("myrepo/mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("myrepo/mychart:1.5.0", ref.FullName())
s = "mychart:5001:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("mychart:5001", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("mychart:5001:1.5.0", ref.FullName())
s = "myrepo:5001/mychart:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("myrepo:5001/mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("myrepo:5001/mychart:1.5.0", ref.FullName())
s = "127.0.0.1:5001/mychart:1.5.0"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("127.0.0.1:5001/mychart", ref.Repo)
is.Equal("1.5.0", ref.Tag)
is.Equal("127.0.0.1:5001/mychart:1.5.0", ref.FullName())
s = "localhost:5000/mychart:latest"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("localhost:5000/mychart", ref.Repo)
is.Equal("latest", ref.Tag)
is.Equal("localhost:5000/mychart:latest", ref.FullName())
s = "my.host.com/my/nested/repo:1.2.3"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("my.host.com/my/nested/repo", ref.Repo)
is.Equal("1.2.3", ref.Tag)
is.Equal("my.host.com/my/nested/repo:1.2.3", ref.FullName())
s = "localhost:5000/x/y/z"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("localhost:5000/x/y/z", ref.Repo)
is.Equal("", ref.Tag)
is.Equal("localhost:5000/x/y/z", ref.FullName())
s = "localhost:5000/x/y/z:123"
ref, err = ParseReference(s)
is.NoError(err)
is.Equal("localhost:5000/x/y/z", ref.Repo)
is.Equal("123", ref.Tag)
is.Equal("localhost:5000/x/y/z:123", ref.FullName())
s = "localhost:5000/x/y/z:123:x"
_, err = ParseReference(s)
is.Error(err, "ref contains too many colons (3)")
s = "localhost:5000/x/y/z:123:x:y"
_, err = ParseReference(s)
is.Error(err, "ref contains too many colons (4)")
}

@ -17,41 +17,31 @@ limitations under the License.
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"bytes"
"context"
"fmt"
"io"
"time"
"strings"
units "github.com/docker/go-units"
"github.com/sirupsen/logrus"
orascontext "oras.land/oras-go/pkg/context"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
)
// byteCountBinary produces a human-readable file size
func byteCountBinary(b int64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
// IsOCI determines whether or not a URL is to be treated as an OCI URL
func IsOCI(url string) bool {
return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
}
// shortDigest returns first 7 characters of a sha256 digest
func shortDigest(digest string) string {
if len(digest) == 64 {
return digest[:7]
// extractChartMeta is used to extract a chart metadata from a byte array
func extractChartMeta(chartData []byte) (*chart.Metadata, error) {
ch, err := loader.LoadArchive(bytes.NewReader(chartData))
if err != nil {
return nil, err
}
return digest
}
// timeAgo returns a human-readable timestamp representing time that has passed
func timeAgo(t time.Time) string {
return units.HumanDuration(time.Now().UTC().Sub(t))
return ch.Metadata, nil
}
// ctx retrieves a fresh context.

@ -0,0 +1,58 @@
/*
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 uploader
import (
"fmt"
"io"
"net/url"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/pusher"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartUploader handles uploading a chart.
type ChartUploader struct {
// Out is the location to write warning and info messages.
Out io.Writer
// Pusher collection for the operation
Pushers pusher.Providers
// Options provide parameters to be passed along to the Pusher being initialized.
Options []pusher.Option
// RegistryClient is a client for interacting with registries.
RegistryClient *registry.Client
}
// UploadTo uploads a chart. Depending on the settings, it may also upload a provenance file.
func (c *ChartUploader) UploadTo(ref, remote string) error {
u, err := url.Parse(remote)
if err != nil {
return errors.Errorf("invalid chart URL format: %s", remote)
}
if u.Scheme == "" {
return errors.New(fmt.Sprintf("scheme prefix missing from remote (e.g. \"%s://\")", registry.OCIScheme))
}
p, err := c.Pushers.ByScheme(u.Scheme)
if err != nil {
return err
}
return p.Push(ref, u.String(), c.Options...)
}

@ -1,11 +1,10 @@
/*
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
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,
@ -14,15 +13,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package registry // import "helm.sh/helm/v3/internal/experimental/registry"
import (
"oras.land/oras-go/pkg/auth"
)
/*Package uploader provides a library for uploading charts.
type (
// Authorizer handles registry auth operations
Authorizer struct {
auth.Client
}
)
This package contains tools for uploading charts to registries.
*/
package uploader

@ -26,6 +26,7 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/gates"
@ -121,7 +122,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
var version string
var ok bool
found := true
if !strings.HasPrefix(d.Repository, "oci://") {
if !registry.IsOCI(d.Repository) {
repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName)))
if err != nil {
return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName)

@ -16,16 +16,12 @@ limitations under the License.
package action
import (
"context"
"flag"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"testing"
fakeclientset "k8s.io/client-go/kubernetes/fake"
dockerauth "oras.land/oras-go/pkg/auth/docker"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
@ -42,16 +38,6 @@ var verbose = flag.Bool("test.log", false, "enable test logging")
func actionConfigFixture(t *testing.T) *Configuration {
t.Helper()
client, err := dockerauth.NewClient()
if err != nil {
t.Fatal(err)
}
resolver, err := client.Resolver(context.Background(), http.DefaultClient, false)
if err != nil {
t.Fatal(err)
}
tdir, err := ioutil.TempDir("", "helm-action-test")
if err != nil {
t.Fatal(err)
@ -59,23 +45,7 @@ func actionConfigFixture(t *testing.T) *Configuration {
t.Cleanup(func() { os.RemoveAll(tdir) })
cache, err := registry.NewCache(
registry.CacheOptDebug(true),
registry.CacheOptRoot(filepath.Join(tdir, registry.CacheRootDir)),
)
if err != nil {
t.Fatal(err)
}
registryClient, err := registry.NewClient(
registry.ClientOptAuthorizer(&registry.Authorizer{
Client: client,
}),
registry.ClientOptResolver(&registry.Resolver{
Resolver: resolver,
}),
registry.ClientOptCache(cache),
)
registryClient, err := registry.NewClient()
if err != nil {
t.Fatal(err)
}

@ -1,63 +0,0 @@
/*
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 (
"fmt"
"io"
"path/filepath"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chartutil"
)
// ChartExport performs a chart export operation.
type ChartExport struct {
cfg *Configuration
Destination string
}
// NewChartExport creates a new ChartExport object with the given configuration.
func NewChartExport(cfg *Configuration) *ChartExport {
return &ChartExport{
cfg: cfg,
}
}
// Run executes the chart export operation
func (a *ChartExport) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
ch, err := a.cfg.RegistryClient.LoadChart(r)
if err != nil {
return err
}
// Save the chart to local destination directory
err = chartutil.SaveDir(ch, a.Destination)
if err != nil {
return err
}
d := filepath.Join(a.Destination, ch.Metadata.Name)
fmt.Fprintf(out, "Exported chart to %s/\n", d)
return nil
}

@ -1,44 +0,0 @@
/*
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"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartList performs a chart list operation.
type ChartList struct {
cfg *Configuration
ColumnWidth uint
}
// NewChartList creates a new ChartList object with the given configuration.
func NewChartList(cfg *Configuration) *ChartList {
return &ChartList{
cfg: cfg,
}
}
// Run executes the chart list operation
func (a *ChartList) Run(out io.Writer) error {
client := a.cfg.RegistryClient
opt := registry.ClientOptColumnWidth(a.ColumnWidth)
opt(client)
return client.PrintChartTable()
}

@ -1,44 +0,0 @@
/*
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"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartPull performs a chart pull operation.
type ChartPull struct {
cfg *Configuration
}
// NewChartPull creates a new ChartPull object with the given configuration.
func NewChartPull(cfg *Configuration) *ChartPull {
return &ChartPull{
cfg: cfg,
}
}
// Run executes the chart pull operation
func (a *ChartPull) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.PullChartToCache(r)
}

@ -1,44 +0,0 @@
/*
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"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartPush performs a chart push operation.
type ChartPush struct {
cfg *Configuration
}
// NewChartPush creates a new ChartPush object with the given configuration.
func NewChartPush(cfg *Configuration) *ChartPush {
return &ChartPush{
cfg: cfg,
}
}
// Run executes the chart push operation
func (a *ChartPush) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.PushChart(r)
}

@ -1,44 +0,0 @@
/*
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"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartRemove performs a chart remove operation.
type ChartRemove struct {
cfg *Configuration
}
// NewChartRemove creates a new ChartRemove object with the given configuration.
func NewChartRemove(cfg *Configuration) *ChartRemove {
return &ChartRemove{
cfg: cfg,
}
}
// Run executes the chart remove operation
func (a *ChartRemove) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.RemoveChart(r)
}

@ -1,51 +0,0 @@
/*
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"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
)
// ChartSave performs a chart save operation.
type ChartSave struct {
cfg *Configuration
}
// NewChartSave creates a new ChartSave object with the given configuration.
func NewChartSave(cfg *Configuration) *ChartSave {
return &ChartSave{
cfg: cfg,
}
}
// Run executes the chart save operation
func (a *ChartSave) Run(out io.Writer, ch *chart.Chart, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
// If no tag is present, use the chart version
if r.Tag == "" {
r.Tag = ch.Metadata.Version
}
return a.cfg.RegistryClient.SaveChart(ch, r)
}

@ -1,70 +0,0 @@
/*
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/ioutil"
"testing"
"helm.sh/helm/v3/internal/experimental/registry"
)
func chartSaveAction(t *testing.T) *ChartSave {
t.Helper()
config := actionConfigFixture(t)
action := NewChartSave(config)
return action
}
func TestChartSave(t *testing.T) {
action := chartSaveAction(t)
input := buildChart()
if err := action.Run(ioutil.Discard, input, "localhost:5000/test:0.2.0"); err != nil {
t.Error(err)
}
ref, err := registry.ParseReference("localhost:5000/test:0.2.0")
if err != nil {
t.Fatal(err)
}
if _, err := action.cfg.RegistryClient.LoadChart(ref); err != nil {
t.Error(err)
}
// now let's check if `helm chart save` can use the chart version when the tag is not present
if err := action.Run(ioutil.Discard, input, "localhost:5000/test"); err != nil {
t.Error(err)
}
ref, err = registry.ParseReference("localhost:5000/test")
if err != nil {
t.Fatal(err)
}
// TODO: guess latest based on semver?
_, err = action.cfg.RegistryClient.LoadChart(ref)
if err == nil {
t.Error("Expected error parsing ref without tag")
}
ref.Tag = "0.1.0"
if _, err := action.cfg.RegistryClient.LoadChart(ref); err != nil {
t.Error(err)
}
}

@ -36,6 +36,7 @@ import (
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
@ -663,6 +664,14 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
}
if registry.IsOCI(name) {
if version == "" {
return "", errors.New("version is explicitly required for OCI registries")
}
dl.Options = append(dl.Options, getter.WithTagName(version))
}
if c.Verify {
dl.Verify = downloader.VerifyAlways
}
@ -716,5 +725,6 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
if version != "" {
atVersion = fmt.Sprintf(" at version %q", version)
}
return filename, errors.Errorf("failed to download %q%s (hint: running `helm repo update` may help)", name, atVersion)
return filename, errors.Errorf("failed to download %q%s", name, atVersion)
}

@ -25,6 +25,7 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader"
@ -90,7 +91,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
RepositoryCache: p.Settings.RepositoryCache,
}
if strings.HasPrefix(chartRef, "oci://") {
if registry.IsOCI(chartRef) {
if p.Version == "" {
return out.String(), errors.Errorf("--version flag is explicitly required for OCI registries")
}

@ -102,7 +102,7 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven
}
name := filepath.Base(u.Path)
if u.Scheme == "oci" {
if u.Scheme == registry.OCIScheme {
name = fmt.Sprintf("%s-%s.tgz", name, version)
}

@ -169,7 +169,7 @@ var httpProvider = Provider{
}
var ociProvider = Provider{
Schemes: []string{"oci"},
Schemes: []string{registry.OCIScheme},
New: NewOCIGetter,
}

@ -39,22 +39,30 @@ func (g *OCIGetter) Get(href string, options ...Option) (*bytes.Buffer, error) {
func (g *OCIGetter) get(href string) (*bytes.Buffer, error) {
client := g.opts.registryClient
ref := strings.TrimPrefix(href, "oci://")
ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme))
var pullOpts []registry.PullOption
requestingProv := strings.HasSuffix(ref, ".prov")
if requestingProv {
ref = strings.TrimSuffix(ref, ".prov")
pullOpts = append(pullOpts,
registry.PullOptWithChart(false),
registry.PullOptWithProv(true))
}
if version := g.opts.version; version != "" {
ref = fmt.Sprintf("%s:%s", ref, version)
}
r, err := registry.ParseReference(ref)
result, err := client.Pull(ref, pullOpts...)
if err != nil {
return nil, err
}
buf, err := client.PullChart(r)
if err != nil {
return nil, err
if requestingProv {
return bytes.NewBuffer(result.Prov.Data), nil
}
return buf, nil
return bytes.NewBuffer(result.Chart.Data), nil
}
// NewOCIGetter constructs a valid http/https client as a Getter

@ -32,7 +32,6 @@ import (
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry
"github.com/phayes/freeport"
"golang.org/x/crypto/bcrypt"
auth "oras.land/oras-go/pkg/auth/docker"
"sigs.k8s.io/yaml"
ociRegistry "helm.sh/helm/v3/internal/experimental/registry"
@ -151,40 +150,25 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) {
credentialsFile := filepath.Join(srv.Dir, "config.json")
client, err := auth.NewClient(credentialsFile)
if err != nil {
t.Fatalf("error creating auth client")
}
resolver, err := client.Resolver(context.Background(), http.DefaultClient, false)
if err != nil {
t.Fatalf("error creating resolver")
}
// init test client
registryClient, err := ociRegistry.NewClient(
ociRegistry.ClientOptDebug(true),
ociRegistry.ClientOptWriter(os.Stdout),
ociRegistry.ClientOptAuthorizer(&ociRegistry.Authorizer{
Client: client,
}),
ociRegistry.ClientOptResolver(&ociRegistry.Resolver{
Resolver: resolver,
}),
ociRegistry.ClientOptCredentialsFile(credentialsFile),
)
if err != nil {
t.Fatalf("error creating registry client")
}
err = registryClient.Login(srv.RegistryURL, srv.TestUsername, srv.TestPassword, false)
err = registryClient.Login(
srv.RegistryURL,
ociRegistry.LoginOptBasicAuth(srv.TestUsername, srv.TestPassword),
ociRegistry.LoginOptInsecure(false))
if err != nil {
t.Fatalf("error logging into registry with good credentials")
}
ref, err := ociRegistry.ParseReference(fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL))
if err != nil {
t.Fatalf("")
}
ref := fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL)
err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz"))
if err != nil {
@ -202,35 +186,56 @@ func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) {
t.Fatal("error removing chart before push")
}
err = registryClient.SaveChart(ch, ref)
// save it back to disk..
absPath, err := chartutil.Save(ch, srv.Dir)
if err != nil {
t.Fatal("error saving chart")
t.Fatal("could not create chart archive")
}
err = registryClient.PushChart(ref)
// load it into memory...
contentBytes, err := ioutil.ReadFile(absPath)
if err != nil {
t.Fatal("error pushing chart")
t.Fatal("could not load chart into memory")
}
if cfg.DependingChart != nil {
c := cfg.DependingChart
dependingRef, err := ociRegistry.ParseReference(fmt.Sprintf("%s/u/ocitestuser/oci-depending-chart:1.2.3", srv.RegistryURL))
if err != nil {
t.Fatal("error parsing reference for depending chart reference")
}
result, err := registryClient.Push(contentBytes, ref)
if err != nil {
t.Fatalf("error pushing dependent chart: %s", err)
}
t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+
"Config.Digest: %s, Config.Size: %d, "+
"Chart.Digest: %s, Chart.Size: %d",
result.Manifest.Digest, result.Manifest.Size,
result.Config.Digest, result.Config.Size,
result.Chart.Digest, result.Chart.Size)
err = registryClient.SaveChart(c, dependingRef)
if err != nil {
t.Fatal("error saving depending chart")
}
srv.Client = registryClient
c := cfg.DependingChart
if c == nil {
return
}
err = registryClient.PushChart(dependingRef)
if err != nil {
t.Fatal("error pushing depending chart")
}
dependingRef := fmt.Sprintf("%s/u/ocitestuser/%s:%s",
srv.RegistryURL, c.Metadata.Name, c.Metadata.Version)
// load it into memory...
absPath = filepath.Join(srv.Dir,
fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version))
contentBytes, err = ioutil.ReadFile(absPath)
if err != nil {
t.Fatal("could not load chart into memory")
}
srv.Client = registryClient
result, err = registryClient.Push(contentBytes, dependingRef)
if err != nil {
t.Fatalf("error pushing depending chart: %s", err)
}
t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+
"Config.Digest: %s, Config.Size: %d, "+
"Chart.Digest: %s, Chart.Size: %d",
result.Manifest.Digest, result.Manifest.Size,
result.Config.Digest, result.Config.Size,
result.Chart.Digest, result.Chart.Size)
}
// NewTempServer creates a server inside of a temp dir.

Loading…
Cancel
Save