diff --git a/cmd/helm/chart.go b/cmd/helm/chart.go index dd4af9273..b1381d355 100644 --- a/cmd/helm/chart.go +++ b/cmd/helm/chart.go @@ -41,10 +41,7 @@ func newChartCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cmd.AddCommand( newChartListCmd(cfg, out), newChartExportCmd(cfg, out), - newChartPullCmd(cfg, out), - newChartPushCmd(cfg, out), newChartRemoveCmd(cfg, out), - newChartSaveCmd(cfg, out), ) return cmd } diff --git a/cmd/helm/chart_pull.go b/cmd/helm/chart_pull.go deleted file mode 100644 index ade952cb8..000000000 --- a/cmd/helm/chart_pull.go +++ /dev/null @@ -1,45 +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/cmd/helm/require" - "helm.sh/helm/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), - RunE: func(cmd *cobra.Command, args []string) error { - ref := args[0] - return action.NewChartPull(cfg).Run(out, ref) - }, - } -} diff --git a/cmd/helm/chart_push.go b/cmd/helm/chart_push.go deleted file mode 100644 index 6f5591b97..000000000 --- a/cmd/helm/chart_push.go +++ /dev/null @@ -1,47 +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/cmd/helm/require" - "helm.sh/helm/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), - RunE: func(cmd *cobra.Command, args []string) error { - ref := args[0] - return action.NewChartPush(cfg).Run(out, ref) - }, - } -} diff --git a/cmd/helm/chart_save.go b/cmd/helm/chart_save.go deleted file mode 100644 index c04bde9ba..000000000 --- a/cmd/helm/chart_save.go +++ /dev/null @@ -1,47 +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/cmd/helm/require" - "helm.sh/helm/pkg/action" -) - -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), - RunE: func(cmd *cobra.Command, args []string) error { - path := args[0] - ref := args[1] - return action.NewChartSave(cfg).Run(out, path, ref) - }, - } -} diff --git a/cmd/helm/create.go b/cmd/helm/create.go index 0caf11d42..6ab8dee24 100644 --- a/cmd/helm/create.go +++ b/cmd/helm/create.go @@ -81,7 +81,7 @@ func (o *createOptions) run(out io.Writer) error { Type: "application", Version: "0.1.0", AppVersion: "1.0", - APIVersion: chart.APIVersionv1, + APIVersion: chart.APIVersionV1, } if o.starter != "" { diff --git a/cmd/helm/create_test.go b/cmd/helm/create_test.go index 33d3b8eee..6ce88dbb7 100644 --- a/cmd/helm/create_test.go +++ b/cmd/helm/create_test.go @@ -55,7 +55,7 @@ func TestCreateCmd(t *testing.T) { if c.Name() != cname { t.Errorf("Expected %q name, got %q", cname, c.Name()) } - if c.Metadata.APIVersion != chart.APIVersionv1 { + if c.Metadata.APIVersion != chart.APIVersionV1 { t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) } } @@ -73,7 +73,11 @@ func TestCreateStarterCmd(t *testing.T) { // Create a starter. starterchart := hh.Starters() os.Mkdir(starterchart, 0755) - if dest, err := chartutil.Create(&chart.Metadata{Name: "starterchart"}, starterchart); err != nil { + if dest, err := chartutil.Create(&chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "starterchart", + Version: "0.1.0", + }, starterchart); err != nil { t.Fatalf("Could not create chart: %s", err) } else { t.Logf("Created %s", dest) @@ -106,7 +110,7 @@ func TestCreateStarterCmd(t *testing.T) { if c.Name() != cname { t.Errorf("Expected %q name, got %q", cname, c.Name()) } - if c.Metadata.APIVersion != chart.APIVersionv1 { + if c.Metadata.APIVersion != chart.APIVersionV1 { t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) } diff --git a/cmd/helm/dependency.go b/cmd/helm/dependency.go index 3942d9b19..18bd4220e 100644 --- a/cmd/helm/dependency.go +++ b/cmd/helm/dependency.go @@ -82,7 +82,7 @@ the contents of a chart. This will produce an error if the chart cannot be loaded. ` -func newDependencyCmd(out io.Writer) *cobra.Command { +func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "dependency update|build|list", Aliases: []string{"dep", "dependencies"}, @@ -92,8 +92,8 @@ func newDependencyCmd(out io.Writer) *cobra.Command { } cmd.AddCommand(newDependencyListCmd(out)) - cmd.AddCommand(newDependencyUpdateCmd(out)) - cmd.AddCommand(newDependencyBuildCmd(out)) + cmd.AddCommand(newDependencyUpdateCmd(cfg, out)) + cmd.AddCommand(newDependencyBuildCmd(cfg, out)) return cmd } diff --git a/cmd/helm/dependency_build.go b/cmd/helm/dependency_build.go index 1162fa883..7e520a533 100644 --- a/cmd/helm/dependency_build.go +++ b/cmd/helm/dependency_build.go @@ -26,7 +26,6 @@ import ( "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" "helm.sh/helm/pkg/downloader" - "helm.sh/helm/pkg/getter" ) const dependencyBuildDesc = ` @@ -40,7 +39,7 @@ If no lock file is found, 'helm dependency build' will mirror the behavior of 'helm dependency update'. ` -func newDependencyBuildCmd(out io.Writer) *cobra.Command { +func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewDependency() cmd := &cobra.Command{ @@ -56,12 +55,12 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command { man := &downloader.Manager{ Out: out, ChartPath: chartpath, - HelmHome: settings.Home, - Keyring: client.Keyring, - Getters: getter.All(settings), + Client: cfg.RegistryClient, } if client.Verify { - man.Verify = downloader.VerifyIfPossible + // TODO(bacongobbler): plug into pkg/repo or oras for signing during a pull + // + // see comment in pkg/repo/client.go#PullChart } if settings.Debug { man.Debug = true diff --git a/cmd/helm/dependency_update.go b/cmd/helm/dependency_update.go index 623bce755..1321c9ec3 100644 --- a/cmd/helm/dependency_update.go +++ b/cmd/helm/dependency_update.go @@ -24,7 +24,6 @@ import ( "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" "helm.sh/helm/pkg/downloader" - "helm.sh/helm/pkg/getter" ) const dependencyUpDesc = ` @@ -43,7 +42,7 @@ in the Chart.yaml file, but (b) at the wrong version. ` // newDependencyUpdateCmd creates a new dependency update command. -func newDependencyUpdateCmd(out io.Writer) *cobra.Command { +func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewDependency() cmd := &cobra.Command{ @@ -58,15 +57,14 @@ func newDependencyUpdateCmd(out io.Writer) *cobra.Command { chartpath = filepath.Clean(args[0]) } man := &downloader.Manager{ - Out: out, - ChartPath: chartpath, - HelmHome: settings.Home, - Keyring: client.Keyring, - SkipUpdate: client.SkipRefresh, - Getters: getter.All(settings), + Out: out, + ChartPath: chartpath, + Client: cfg.RegistryClient, } if client.Verify { - man.Verify = downloader.VerifyAlways + // TODO(bacongobbler): plug into pkg/repo or oras for signing during a pull + // + // see comment in pkg/repo/client.go#PullChart } if settings.Debug { man.Debug = true diff --git a/cmd/helm/dependency_update_test.go b/cmd/helm/dependency_update_test.go index 7e366bde2..f724cbf68 100644 --- a/cmd/helm/dependency_update_test.go +++ b/cmd/helm/dependency_update_test.go @@ -205,8 +205,9 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) { // The baseURL can be used to point to a particular repository server. func createTestingMetadata(name, baseURL string) *chart.Metadata { return &chart.Metadata{ - Name: name, - Version: "1.2.3", + APIVersion: chart.APIVersionV1, + Name: name, + Version: "1.2.3", Dependencies: []*chart.Dependency{ {Name: "reqtest", Version: "0.1.0", Repository: baseURL}, {Name: "compressedchart", Version: "0.1.0", Repository: baseURL}, diff --git a/cmd/helm/home.go b/cmd/helm/home.go index 2f6e9f31a..533a02411 100644 --- a/cmd/helm/home.go +++ b/cmd/helm/home.go @@ -41,7 +41,6 @@ func newHomeCmd(out io.Writer) *cobra.Command { fmt.Fprintln(out, h) if settings.Debug { fmt.Fprintf(out, "Repository: %s\n", h.Repository()) - fmt.Fprintf(out, "RepositoryFile: %s\n", h.RepositoryFile()) fmt.Fprintf(out, "Cache: %s\n", h.Cache()) fmt.Fprintf(out, "Stable CacheIndex: %s\n", h.CacheIndex("stable")) fmt.Fprintf(out, "Starters: %s\n", h.Starters()) diff --git a/cmd/helm/init.go b/cmd/helm/init.go index a3f6e721d..e8aa84f80 100644 --- a/cmd/helm/init.go +++ b/cmd/helm/init.go @@ -28,26 +28,17 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/cmd/helm/require" - "helm.sh/helm/pkg/getter" "helm.sh/helm/pkg/helmpath" "helm.sh/helm/pkg/plugin" "helm.sh/helm/pkg/plugin/installer" - "helm.sh/helm/pkg/repo" ) const initDesc = ` This command sets up local configuration in $HELM_HOME (default ~/.helm/). ` -const ( - stableRepository = "stable" - defaultStableRepositoryURL = "https://kubernetes-charts.storage.googleapis.com" -) - type initOptions struct { - skipRefresh bool // --skip-refresh - stableRepositoryURL string // --stable-repo-url - pluginsFilename string // --plugins + pluginsFilename string // --plugins home helmpath.Home } @@ -76,8 +67,6 @@ func newInitCmd(out io.Writer) *cobra.Command { } f := cmd.Flags() - f.BoolVar(&o.skipRefresh, "skip-refresh", false, "do not refresh (download) the local repository cache") - f.StringVar(&o.stableRepositoryURL, "stable-repo-url", defaultStableRepositoryURL, "URL for stable repository") f.StringVar(&o.pluginsFilename, "plugins", "", "a YAML file specifying plugins to install") return cmd @@ -88,12 +77,6 @@ func (o *initOptions) run(out io.Writer) error { if err := ensureDirectories(o.home, out); err != nil { return err } - if err := ensureDefaultRepos(o.home, out, o.skipRefresh, o.stableRepositoryURL); err != nil { - return err - } - if err := ensureRepoFileFormat(o.home.RepositoryFile(), out); err != nil { - return err - } if o.pluginsFilename != "" { if err := ensurePluginsInstalled(o.pluginsFilename, out); err != nil { return err @@ -130,61 +113,6 @@ func ensureDirectories(home helmpath.Home, out io.Writer) error { return nil } -func ensureDefaultRepos(home helmpath.Home, out io.Writer, skipRefresh bool, url string) error { - repoFile := home.RepositoryFile() - if fi, err := os.Stat(repoFile); err != nil { - fmt.Fprintf(out, "Creating %s \n", repoFile) - f := repo.NewFile() - sr, err := initRepo(url, home.CacheIndex(stableRepository), out, skipRefresh, home) - if err != nil { - return err - } - f.Add(sr) - if err := f.WriteFile(repoFile, 0644); err != nil { - return err - } - } else if fi.IsDir() { - return errors.Errorf("%s must be a file, not a directory", repoFile) - } - return nil -} - -func initRepo(url, cacheFile string, out io.Writer, skipRefresh bool, home helmpath.Home) (*repo.Entry, error) { - fmt.Fprintf(out, "Adding %s repo with URL: %s \n", stableRepository, url) - c := repo.Entry{ - Name: stableRepository, - URL: url, - Cache: cacheFile, - } - r, err := repo.NewChartRepository(&c, getter.All(settings)) - if err != nil { - return nil, err - } - - if skipRefresh { - return &c, nil - } - - // In this case, the cacheFile is always absolute. So passing empty string - // is safe. - if err := r.DownloadIndexFile(""); err != nil { - return nil, errors.Wrapf(err, "%s is not a valid chart repository or cannot be reached", url) - } - - return &c, nil -} - -func ensureRepoFileFormat(file string, out io.Writer) error { - r, err := repo.LoadFile(file) - if err == repo.ErrRepoOutOfDate { - fmt.Fprintln(out, "Updating repository file format...") - if err := r.WriteFile(file, 0644); err != nil { - return err - } - } - return nil -} - func ensurePluginsInstalled(pluginsFilename string, out io.Writer) error { bytes, err := ioutil.ReadFile(pluginsFilename) if err != nil { diff --git a/cmd/helm/install.go b/cmd/helm/install.go index b0f8919d2..91b54a1c8 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -26,10 +26,7 @@ import ( "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" - "helm.sh/helm/pkg/chart/loader" "helm.sh/helm/pkg/chartutil" - "helm.sh/helm/pkg/downloader" - "helm.sh/helm/pkg/getter" ) const installDesc = ` @@ -103,7 +100,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: installDesc, Args: require.MinimumNArgs(1), RunE: func(_ *cobra.Command, args []string) error { - rel, err := runInstall(args, client, out) + rel, err := runInstall(args, client, cfg, out) if err != nil { return err } @@ -126,7 +123,6 @@ func addInstallFlags(f *pflag.FlagSet, client *action.Install) { f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)") f.StringVar(&client.NameTemplate, "name-template", "", "specify template used to name the release") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") - f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "run helm dependency update before installing the chart") addValueOptionsFlags(f, &client.ValueOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions) } @@ -149,7 +145,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") } -func runInstall(args []string, client *action.Install, out io.Writer) (*release.Release, error) { +func runInstall(args []string, client *action.Install, cfg *action.Configuration, out io.Writer) (*release.Release, error) { debug("Original chart version: %q", client.Version) if client.Version == "" && client.Devel { debug("setting version to >0.0.0-0") @@ -162,23 +158,17 @@ func runInstall(args []string, client *action.Install, out io.Writer) (*release. } client.ReleaseName = name - cp, err := client.ChartPathOptions.LocateChart(chart, settings) + chartRequested, err := action.LocateChart(chart, client.ChartPathOptions, settings, cfg.RegistryClient) if err != nil { return nil, err } - debug("CHART PATH: %s\n", cp) + debug("CHART: %s\n", chartRequested) if err := client.ValueOptions.MergeValues(settings); err != nil { return nil, err } - // Check chart dependencies to make sure all are present in /charts - chartRequested, err := loader.Load(cp) - if err != nil { - return nil, err - } - validInstallableChart, err := chartutil.IsChartInstallable(chartRequested) if !validInstallableChart { return nil, err @@ -189,21 +179,7 @@ func runInstall(args []string, client *action.Install, out io.Writer) (*release. // As of Helm 2.4.0, this is treated as a stopping condition: // https://github.com/helm/helm/issues/2209 if err := action.CheckDependencies(chartRequested, req); err != nil { - if client.DependencyUpdate { - man := &downloader.Manager{ - Out: out, - ChartPath: cp, - HelmHome: settings.Home, - Keyring: client.ChartPathOptions.Keyring, - SkipUpdate: false, - Getters: getter.All(settings), - } - if err := man.Update(); err != nil { - return nil, err - } - } else { - return nil, err - } + return nil, err } } diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 38464b585..05ec6ea1a 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -113,7 +113,7 @@ func TestInstall(t *testing.T) { }, // Install, chart with bad dependencies in Chart.yaml in /charts { - name: "install chart with bad dependencies in Chart.yaml", + name: "install chart with bad dependencies in Chart.yaml", cmd: "install badreq testdata/testcharts/chart-bad-requirements", wantError: true, }, diff --git a/cmd/helm/package.go b/cmd/helm/package.go index 09241fea7..e67a4f21a 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -28,7 +28,6 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/pkg/downloader" - "helm.sh/helm/pkg/getter" ) const packageDesc = ` @@ -42,8 +41,8 @@ Chart.yaml file, and (if found) build the current directory into a chart. Versioned chart archives are used by Helm package repositories. ` -func newPackageCmd(out io.Writer) *cobra.Command { - client := action.NewPackage() +func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewPackage(cfg) cmd := &cobra.Command{ Use: "package [CHART_PATH] [...]", @@ -75,9 +74,7 @@ func newPackageCmd(out io.Writer) *cobra.Command { downloadManager := &downloader.Manager{ Out: ioutil.Discard, ChartPath: path, - HelmHome: settings.Home, - Keyring: client.Keyring, - Getters: getter.All(settings), + Client: cfg.RegistryClient, Debug: settings.Debug, } @@ -85,11 +82,10 @@ func newPackageCmd(out io.Writer) *cobra.Command { return err } } - p, err := client.Run(path) - if err != nil { + if err := client.Run(path); err != nil { return err } - fmt.Fprintf(out, "Successfully packaged chart and saved it to: %s\n", p) + fmt.Fprintln(out, "Successfully packaged") } return nil }, @@ -101,7 +97,7 @@ func newPackageCmd(out io.Writer) *cobra.Command { f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "location of a public keyring") f.StringVar(&client.Version, "version", "", "set the version on the chart to this semver version") f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") - f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") + f.StringVar(&client.Registry, "registry", "", "name of the registry where this package will be published") f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) addValueOptionsFlags(f, &client.ValueOptions) diff --git a/cmd/helm/pull.go b/cmd/helm/pull.go index 76b4240d4..fbbf2fe3d 100644 --- a/cmd/helm/pull.go +++ b/cmd/helm/pull.go @@ -27,7 +27,7 @@ import ( ) const pullDesc = ` -Retrieve a package from a package repository, and download it locally. +Retrieve a package from a registry and download it locally. This is useful for fetching packages to inspect, modify, or repackage. It can also be used to perform cryptographic verification of a chart without installing @@ -41,12 +41,12 @@ file, and MUST pass the verification process. Failure in any part of this will result in an error, and the chart will not be saved locally. ` -func newPullCmd(out io.Writer) *cobra.Command { - client := action.NewPull() +func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewPull(cfg) cmd := &cobra.Command{ Use: "pull [chart URL | repo/chartname] [...]", - Short: "download a chart from a repository and (optionally) unpack it in local directory", + Short: "download a chart from a registry and (optionally) unpack it in local directory", Aliases: []string{"fetch"}, Long: pullDesc, Args: require.MinimumNArgs(1), diff --git a/cmd/helm/push.go b/cmd/helm/push.go new file mode 100644 index 000000000..1478f8075 --- /dev/null +++ b/cmd/helm/push.go @@ -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 main + +import ( + "io" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "helm.sh/helm/cmd/helm/require" + "helm.sh/helm/pkg/action" +) + +const pushDesc = ` +Upload a chart to a registry. + +This command will take a distributable, compressed chart and upload it to a registry. + +The chart must already exist in the local registry cache, which can be created with either "helm package" or "helm pull". + +Some registries require you to be authenticated with "helm login" before uploading a chart. +` + +func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + return &cobra.Command{ + Use: "push [...]", + Short: "upload a chart to a registry", + Long: pushDesc, + Args: require.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return errors.Errorf("need at least one argument: the name of the chart to push") + } + for i := 0; i < len(args); i++ { + ref := args[i] + if err := action.NewChartPush(cfg).Run(out, ref); err != nil { + return err + } + } + return nil + }, + } +} diff --git a/cmd/helm/repo.go b/cmd/helm/repo.go deleted file mode 100644 index afd5850d5..000000000 --- a/cmd/helm/repo.go +++ /dev/null @@ -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/cmd/helm/require" -) - -var repoHelm = ` -This command consists of multiple subcommands to interact with chart repositories. - -It can be used to add, remove, list, and index chart repositories. -Example usage: - $ helm repo add [NAME] [REPO_URL] -` - -func newRepoCmd(out io.Writer) *cobra.Command { - cmd := &cobra.Command{ - Use: "repo add|remove|list|index|update [ARGS]", - Short: "add, list, remove, update, and index chart repositories", - Long: repoHelm, - Args: require.NoArgs, - } - - cmd.AddCommand(newRepoAddCmd(out)) - cmd.AddCommand(newRepoListCmd(out)) - cmd.AddCommand(newRepoRemoveCmd(out)) - cmd.AddCommand(newRepoIndexCmd(out)) - cmd.AddCommand(newRepoUpdateCmd(out)) - - return cmd -} diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go deleted file mode 100644 index 9df0f946a..000000000 --- a/cmd/helm/repo_add.go +++ /dev/null @@ -1,114 +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 ( - "fmt" - "io" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "helm.sh/helm/cmd/helm/require" - "helm.sh/helm/pkg/getter" - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/repo" -) - -type repoAddOptions struct { - name string - url string - username string - password string - home helmpath.Home - noupdate bool - - certFile string - keyFile string - caFile string -} - -func newRepoAddCmd(out io.Writer) *cobra.Command { - o := &repoAddOptions{} - - cmd := &cobra.Command{ - Use: "add [NAME] [URL]", - Short: "add a chart repository", - Args: require.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - o.name = args[0] - o.url = args[1] - o.home = settings.Home - - return o.run(out) - }, - } - - f := cmd.Flags() - f.StringVar(&o.username, "username", "", "chart repository username") - f.StringVar(&o.password, "password", "", "chart repository password") - f.BoolVar(&o.noupdate, "no-update", false, "raise error if repo is already registered") - f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") - f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") - f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") - - return cmd -} - -func (o *repoAddOptions) run(out io.Writer) error { - if err := addRepository(o.name, o.url, o.username, o.password, o.home, o.certFile, o.keyFile, o.caFile, o.noupdate); err != nil { - return err - } - fmt.Fprintf(out, "%q has been added to your repositories\n", o.name) - return nil -} - -func addRepository(name, url, username, password string, home helmpath.Home, certFile, keyFile, caFile string, noUpdate bool) error { - f, err := repo.LoadFile(home.RepositoryFile()) - if err != nil { - return err - } - - if noUpdate && f.Has(name) { - return errors.Errorf("repository name (%s) already exists, please specify a different name", name) - } - - cif := home.CacheIndex(name) - c := repo.Entry{ - Name: name, - Cache: cif, - URL: url, - Username: username, - Password: password, - CertFile: certFile, - KeyFile: keyFile, - CAFile: caFile, - } - - r, err := repo.NewChartRepository(&c, getter.All(settings)) - if err != nil { - return err - } - - if err := r.DownloadIndexFile(home.Cache()); err != nil { - return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", url) - } - - f.Update(&c) - - return f.WriteFile(home.RepositoryFile(), 0644) -} diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go deleted file mode 100644 index 9fd33390a..000000000 --- a/cmd/helm/repo_add_test.go +++ /dev/null @@ -1,89 +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 ( - "fmt" - "os" - "testing" - - "helm.sh/helm/pkg/repo" - "helm.sh/helm/pkg/repo/repotest" -) - -func TestRepoAddCmd(t *testing.T) { - defer resetEnv()() - - srv, hh, err := repotest.NewTempServer("testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } - - defer func() { - srv.Stop() - os.RemoveAll(hh.String()) - }() - ensureTestHome(t, hh) - settings.Home = hh - - tests := []cmdTestCase{{ - name: "add a repository", - cmd: fmt.Sprintf("repo add test-name %s --home '%s'", srv.URL(), hh), - golden: "output/repo-add.txt", - }} - - runTestCmd(t, tests) -} - -func TestRepoAdd(t *testing.T) { - defer resetEnv()() - - ts, hh, err := repotest.NewTempServer("testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } - - defer func() { - ts.Stop() - os.RemoveAll(hh.String()) - }() - ensureTestHome(t, hh) - settings.Home = hh - - const testRepoName = "test-name" - - if err := addRepository(testRepoName, ts.URL(), "", "", hh, "", "", "", true); err != nil { - t.Error(err) - } - - f, err := repo.LoadFile(hh.RepositoryFile()) - if err != nil { - t.Error(err) - } - - if !f.Has(testRepoName) { - t.Errorf("%s was not successfully inserted into %s", testRepoName, hh.RepositoryFile()) - } - - if err := addRepository(testRepoName, ts.URL(), "", "", hh, "", "", "", false); err != nil { - t.Errorf("Repository was not updated: %s", err) - } - - if err := addRepository(testRepoName, ts.URL(), "", "", hh, "", "", "", false); err != nil { - t.Errorf("Duplicate repository name was added") - } -} diff --git a/cmd/helm/repo_index.go b/cmd/helm/repo_index.go deleted file mode 100644 index 677f532b1..000000000 --- a/cmd/helm/repo_index.go +++ /dev/null @@ -1,101 +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" - "os" - "path/filepath" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "helm.sh/helm/cmd/helm/require" - "helm.sh/helm/pkg/repo" -) - -const repoIndexDesc = ` -Read the current directory and generate an index file based on the charts found. - -This tool is used for creating an 'index.yaml' file for a chart repository. To -set an absolute URL to the charts, use '--url' flag. - -To merge the generated index with an existing index file, use the '--merge' -flag. In this case, the charts found in the current directory will be merged -into the existing index, with local charts taking priority over existing charts. -` - -type repoIndexOptions struct { - dir string - url string - merge string -} - -func newRepoIndexCmd(out io.Writer) *cobra.Command { - o := &repoIndexOptions{} - - cmd := &cobra.Command{ - Use: "index [DIR]", - Short: "generate an index file given a directory containing packaged charts", - Long: repoIndexDesc, - Args: require.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - o.dir = args[0] - return o.run(out) - }, - } - - f := cmd.Flags() - f.StringVar(&o.url, "url", "", "url of chart repository") - f.StringVar(&o.merge, "merge", "", "merge the generated index into the given index") - - return cmd -} - -func (i *repoIndexOptions) run(out io.Writer) error { - path, err := filepath.Abs(i.dir) - if err != nil { - return err - } - - return index(path, i.url, i.merge) -} - -func index(dir, url, mergeTo string) error { - out := filepath.Join(dir, "index.yaml") - - i, err := repo.IndexDirectory(dir, url) - if err != nil { - return err - } - if mergeTo != "" { - // if index.yaml is missing then create an empty one to merge into - var i2 *repo.IndexFile - if _, err := os.Stat(mergeTo); os.IsNotExist(err) { - i2 = repo.NewIndexFile() - i2.WriteFile(mergeTo, 0755) - } else { - i2, err = repo.LoadIndexFile(mergeTo) - if err != nil { - return errors.Wrap(err, "merge failed") - } - } - i.Merge(i2) - } - i.SortEntries() - return i.WriteFile(out, 0755) -} diff --git a/cmd/helm/repo_index_test.go b/cmd/helm/repo_index_test.go deleted file mode 100644 index b66bc565a..000000000 --- a/cmd/helm/repo_index_test.go +++ /dev/null @@ -1,166 +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 ( - "bytes" - "io" - "os" - "path/filepath" - "testing" - - "helm.sh/helm/pkg/repo" -) - -func TestRepoIndexCmd(t *testing.T) { - - dir := testTempDir(t) - - comp := filepath.Join(dir, "compressedchart-0.1.0.tgz") - if err := linkOrCopy("testdata/testcharts/compressedchart-0.1.0.tgz", comp); err != nil { - t.Fatal(err) - } - comp2 := filepath.Join(dir, "compressedchart-0.2.0.tgz") - if err := linkOrCopy("testdata/testcharts/compressedchart-0.2.0.tgz", comp2); err != nil { - t.Fatal(err) - } - - buf := bytes.NewBuffer(nil) - c := newRepoIndexCmd(buf) - - if err := c.RunE(c, []string{dir}); err != nil { - t.Error(err) - } - - destIndex := filepath.Join(dir, "index.yaml") - - index, err := repo.LoadIndexFile(destIndex) - if err != nil { - t.Fatal(err) - } - - if len(index.Entries) != 1 { - t.Errorf("expected 1 entry, got %d: %#v", len(index.Entries), index.Entries) - } - - vs := index.Entries["compressedchart"] - if len(vs) != 2 { - t.Errorf("expected 2 versions, got %d: %#v", len(vs), vs) - } - - expectedVersion := "0.2.0" - if vs[0].Version != expectedVersion { - t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) - } - - // Test with `--merge` - - // Remove first two charts. - if err := os.Remove(comp); err != nil { - t.Fatal(err) - } - if err := os.Remove(comp2); err != nil { - t.Fatal(err) - } - // Add a new chart and a new version of an existing chart - if err := linkOrCopy("testdata/testcharts/reqtest-0.1.0.tgz", filepath.Join(dir, "reqtest-0.1.0.tgz")); err != nil { - t.Fatal(err) - } - if err := linkOrCopy("testdata/testcharts/compressedchart-0.3.0.tgz", filepath.Join(dir, "compressedchart-0.3.0.tgz")); err != nil { - t.Fatal(err) - } - - c.ParseFlags([]string{"--merge", destIndex}) - if err := c.RunE(c, []string{dir}); err != nil { - t.Error(err) - } - - index, err = repo.LoadIndexFile(destIndex) - if err != nil { - t.Fatal(err) - } - - if len(index.Entries) != 2 { - t.Errorf("expected 2 entries, got %d: %#v", len(index.Entries), index.Entries) - } - - vs = index.Entries["compressedchart"] - if len(vs) != 3 { - t.Errorf("expected 3 versions, got %d: %#v", len(vs), vs) - } - - expectedVersion = "0.3.0" - if vs[0].Version != expectedVersion { - t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) - } - - // test that index.yaml gets generated on merge even when it doesn't exist - if err := os.Remove(destIndex); err != nil { - t.Fatal(err) - } - - c.ParseFlags([]string{"--merge", destIndex}) - if err := c.RunE(c, []string{dir}); err != nil { - t.Error(err) - } - - _, err = repo.LoadIndexFile(destIndex) - if err != nil { - t.Fatal(err) - } - - // verify it didn't create an empty index.yaml and the merged happened - if len(index.Entries) != 2 { - t.Errorf("expected 2 entries, got %d: %#v", len(index.Entries), index.Entries) - } - - vs = index.Entries["compressedchart"] - if len(vs) != 3 { - t.Errorf("expected 3 versions, got %d: %#v", len(vs), vs) - } - - expectedVersion = "0.3.0" - if vs[0].Version != expectedVersion { - t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) - } -} - -func linkOrCopy(old, new string) error { - if err := os.Link(old, new); err != nil { - return copyFile(old, new) - } - - return nil -} - -func copyFile(dst, src string) error { - i, err := os.Open(dst) - if err != nil { - return err - } - defer i.Close() - - o, err := os.Create(src) - if err != nil { - return err - } - defer o.Close() - - _, err = io.Copy(o, i) - - return err -} diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go deleted file mode 100644 index b20e652dd..000000000 --- a/cmd/helm/repo_list.go +++ /dev/null @@ -1,67 +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 ( - "fmt" - "io" - - "github.com/gosuri/uitable" - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "helm.sh/helm/cmd/helm/require" - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/repo" -) - -type repoListOptions struct { - home helmpath.Home -} - -func newRepoListCmd(out io.Writer) *cobra.Command { - o := &repoListOptions{} - - cmd := &cobra.Command{ - Use: "list", - Short: "list chart repositories", - Args: require.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - o.home = settings.Home - return o.run(out) - }, - } - - return cmd -} - -func (o *repoListOptions) run(out io.Writer) error { - f, err := repo.LoadFile(o.home.RepositoryFile()) - if err != nil { - return err - } - if len(f.Repositories) == 0 { - return errors.New("no repositories to show") - } - table := uitable.New() - table.AddRow("NAME", "URL") - for _, re := range f.Repositories { - table.AddRow(re.Name, re.URL) - } - fmt.Fprintln(out, table) - return nil -} diff --git a/cmd/helm/repo_remove.go b/cmd/helm/repo_remove.go deleted file mode 100644 index 75eb1a9bd..000000000 --- a/cmd/helm/repo_remove.go +++ /dev/null @@ -1,91 +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 ( - "fmt" - "io" - "os" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "helm.sh/helm/cmd/helm/require" - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/repo" -) - -type repoRemoveOptions struct { - name string - home helmpath.Home -} - -func newRepoRemoveCmd(out io.Writer) *cobra.Command { - o := &repoRemoveOptions{} - - cmd := &cobra.Command{ - Use: "remove [NAME]", - Aliases: []string{"rm"}, - Short: "remove a chart repository", - Args: require.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - o.name = args[0] - o.home = settings.Home - - return o.run(out) - }, - } - - return cmd -} - -func (r *repoRemoveOptions) run(out io.Writer) error { - return removeRepoLine(out, r.name, r.home) -} - -func removeRepoLine(out io.Writer, name string, home helmpath.Home) error { - repoFile := home.RepositoryFile() - r, err := repo.LoadFile(repoFile) - if err != nil { - return err - } - - if !r.Remove(name) { - return errors.Errorf("no repo named %q found", name) - } - if err := r.WriteFile(repoFile, 0644); err != nil { - return err - } - - if err := removeRepoCache(name, home); err != nil { - return err - } - - fmt.Fprintf(out, "%q has been removed from your repositories\n", name) - - return nil -} - -func removeRepoCache(name string, home helmpath.Home) error { - if _, err := os.Stat(home.CacheIndex(name)); err == nil { - err = os.Remove(home.CacheIndex(name)) - if err != nil { - return err - } - } - return nil -} diff --git a/cmd/helm/repo_remove_test.go b/cmd/helm/repo_remove_test.go deleted file mode 100644 index 44bd20d70..000000000 --- a/cmd/helm/repo_remove_test.go +++ /dev/null @@ -1,77 +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 ( - "bytes" - "os" - "strings" - "testing" - - "helm.sh/helm/pkg/repo" - "helm.sh/helm/pkg/repo/repotest" -) - -func TestRepoRemove(t *testing.T) { - defer resetEnv()() - - ts, hh, err := repotest.NewTempServer("testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } - - defer func() { - ts.Stop() - os.RemoveAll(hh.String()) - }() - ensureTestHome(t, hh) - settings.Home = hh - - const testRepoName = "test-name" - - b := bytes.NewBuffer(nil) - if err := removeRepoLine(b, testRepoName, hh); err == nil { - t.Errorf("Expected error removing %s, but did not get one.", testRepoName) - } - if err := addRepository(testRepoName, ts.URL(), "", "", hh, "", "", "", true); err != nil { - t.Error(err) - } - - mf, _ := os.Create(hh.CacheIndex(testRepoName)) - mf.Close() - - b.Reset() - if err := removeRepoLine(b, testRepoName, hh); err != nil { - t.Errorf("Error removing %s from repositories", testRepoName) - } - if !strings.Contains(b.String(), "has been removed") { - t.Errorf("Unexpected output: %s", b.String()) - } - - if _, err := os.Stat(hh.CacheIndex(testRepoName)); err == nil { - t.Errorf("Error cache file was not removed for repository %s", testRepoName) - } - - f, err := repo.LoadFile(hh.RepositoryFile()) - if err != nil { - t.Error(err) - } - - if f.Has(testRepoName) { - t.Errorf("%s was not successfully removed from repositories list", testRepoName) - } -} diff --git a/cmd/helm/repo_update.go b/cmd/helm/repo_update.go deleted file mode 100644 index a941c0867..000000000 --- a/cmd/helm/repo_update.go +++ /dev/null @@ -1,103 +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 ( - "fmt" - "io" - "sync" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "helm.sh/helm/cmd/helm/require" - "helm.sh/helm/pkg/getter" - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/repo" -) - -const updateDesc = ` -Update gets the latest information about charts from the respective chart repositories. -Information is cached locally, where it is used by commands like 'helm search'. - -'helm update' is the deprecated form of 'helm repo update'. It will be removed in -future releases. -` - -var errNoRepositories = errors.New("no repositories found. You must add one before updating") - -type repoUpdateOptions struct { - update func([]*repo.ChartRepository, io.Writer, helmpath.Home) - home helmpath.Home -} - -func newRepoUpdateCmd(out io.Writer) *cobra.Command { - o := &repoUpdateOptions{update: updateCharts} - - cmd := &cobra.Command{ - Use: "update", - Aliases: []string{"up"}, - Short: "update information of available charts locally from chart repositories", - Long: updateDesc, - Args: require.NoArgs, - RunE: func(cmd *cobra.Command, args []string) error { - o.home = settings.Home - return o.run(out) - }, - } - return cmd -} - -func (o *repoUpdateOptions) run(out io.Writer) error { - f, err := repo.LoadFile(o.home.RepositoryFile()) - if err != nil { - return err - } - - if len(f.Repositories) == 0 { - return errNoRepositories - } - var repos []*repo.ChartRepository - for _, cfg := range f.Repositories { - r, err := repo.NewChartRepository(cfg, getter.All(settings)) - if err != nil { - return err - } - repos = append(repos, r) - } - - o.update(repos, out, o.home) - return nil -} - -func updateCharts(repos []*repo.ChartRepository, out io.Writer, home helmpath.Home) { - fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") - var wg sync.WaitGroup - for _, re := range repos { - wg.Add(1) - go func(re *repo.ChartRepository) { - defer wg.Done() - if err := re.DownloadIndexFile(home.Cache()); err != nil { - fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) - } else { - fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) - } - }(re) - } - wg.Wait() - fmt.Fprintln(out, "Update Complete. ⎈ Happy Helming!⎈ ") -} diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go deleted file mode 100644 index 17213d9b3..000000000 --- a/cmd/helm/repo_update_test.go +++ /dev/null @@ -1,93 +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 ( - "bytes" - "fmt" - "io" - "os" - "strings" - "testing" - - "helm.sh/helm/pkg/getter" - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/repo" - "helm.sh/helm/pkg/repo/repotest" -) - -func TestUpdateCmd(t *testing.T) { - defer resetEnv()() - - hh := testHelmHome(t) - settings.Home = hh - - out := bytes.NewBuffer(nil) - // Instead of using the HTTP updater, we provide our own for this test. - // The TestUpdateCharts test verifies the HTTP behavior independently. - updater := func(repos []*repo.ChartRepository, out io.Writer, hh helmpath.Home) { - for _, re := range repos { - fmt.Fprintln(out, re.Config.Name) - } - } - o := &repoUpdateOptions{ - update: updater, - home: hh, - } - if err := o.run(out); err != nil { - t.Fatal(err) - } - - if got := out.String(); !strings.Contains(got, "charts") { - t.Errorf("Expected 'charts' got %q", got) - } -} - -func TestUpdateCharts(t *testing.T) { - defer resetEnv()() - - ts, hh, err := repotest.NewTempServer("testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } - - defer func() { - ts.Stop() - os.RemoveAll(hh.String()) - }() - ensureTestHome(t, hh) - settings.Home = hh - - r, err := repo.NewChartRepository(&repo.Entry{ - Name: "charts", - URL: ts.URL(), - Cache: hh.CacheIndex("charts"), - }, getter.All(settings)) - if err != nil { - t.Error(err) - } - - b := bytes.NewBuffer(nil) - updateCharts([]*repo.ChartRepository{r}, b, hh) - - got := b.String() - if strings.Contains(got, "Unable to get an update") { - t.Errorf("Failed to get a repo: %q", got) - } - if !strings.Contains(got, "Update Complete.") { - t.Error("Update was not successful") - } -} diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 5a6f43d6e..2666ff580 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -19,12 +19,11 @@ package main // import "helm.sh/helm/cmd/helm" import ( "io" - "github.com/containerd/containerd/remotes/docker" "github.com/spf13/cobra" "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" - "helm.sh/helm/pkg/registry" + "helm.sh/helm/pkg/repo" ) var globalUsage = `The Kubernetes package manager @@ -66,26 +65,22 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string // set defaults from environment settings.Init(flags) - // Add the registry client based on settings + // Add the repo client based on settings // TODO: Move this elsewhere (first, settings.Init() must move) - actionConfig.RegistryClient = registry.NewClient(®istry.ClientOptions{ - Out: out, - Resolver: registry.Resolver{ - Resolver: docker.NewResolver(docker.ResolverOptions{}), - }, - CacheRootDir: settings.Home.Registry(), + actionConfig.RegistryClient = repo.NewClient(&repo.ClientOptions{ + Out: out, + CacheRootDir: settings.Home.Repository(), }) cmd.AddCommand( // chart commands newCreateCmd(out), - newDependencyCmd(out), - newPullCmd(out), - newShowCmd(out), + newDependencyCmd(actionConfig, out), + newPullCmd(actionConfig, out), + newPushCmd(actionConfig, out), + newShowCmd(actionConfig, out), newLintCmd(out), - newPackageCmd(out), - newRepoCmd(out), - newSearchCmd(out), + newPackageCmd(actionConfig, out), newVerifyCmd(out), newChartCmd(actionConfig, out), @@ -104,7 +99,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string newHomeCmd(out), newInitCmd(out), newPluginCmd(out), - newTemplateCmd(out), + newTemplateCmd(actionConfig, out), newVersionCmd(out), // Hidden documentation generator command: 'helm docs' diff --git a/cmd/helm/search.go b/cmd/helm/search.go deleted file mode 100644 index 25228ee48..000000000 --- a/cmd/helm/search.go +++ /dev/null @@ -1,163 +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 ( - "fmt" - "io" - "strings" - - "github.com/Masterminds/semver" - "github.com/gosuri/uitable" - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "helm.sh/helm/cmd/helm/search" - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/repo" -) - -const searchDesc = ` -Search reads through all of the repositories configured on the system, and -looks for matches. - -Repositories are managed with 'helm repo' commands. -` - -// searchMaxScore suggests that any score higher than this is not considered a match. -const searchMaxScore = 25 - -type searchOptions struct { - helmhome helmpath.Home - - versions bool - regexp bool - version string -} - -func newSearchCmd(out io.Writer) *cobra.Command { - o := &searchOptions{} - - cmd := &cobra.Command{ - Use: "search [keyword]", - Short: "search for a keyword in charts", - Long: searchDesc, - RunE: func(cmd *cobra.Command, args []string) error { - o.helmhome = settings.Home - return o.run(out, args) - }, - } - - f := cmd.Flags() - f.BoolVarP(&o.regexp, "regexp", "r", false, "use regular expressions for searching") - f.BoolVarP(&o.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line") - f.StringVarP(&o.version, "version", "v", "", "search using semantic versioning constraints") - - return cmd -} - -func (o *searchOptions) run(out io.Writer, args []string) error { - index, err := o.buildIndex(out) - if err != nil { - return err - } - - var res []*search.Result - if len(args) == 0 { - res = index.All() - } else { - q := strings.Join(args, " ") - res, err = index.Search(q, searchMaxScore, o.regexp) - if err != nil { - return err - } - } - - search.SortScore(res) - data, err := o.applyConstraint(res) - if err != nil { - return err - } - - fmt.Fprintln(out, o.formatSearchResults(data)) - - return nil -} - -func (o *searchOptions) applyConstraint(res []*search.Result) ([]*search.Result, error) { - if len(o.version) == 0 { - return res, nil - } - - constraint, err := semver.NewConstraint(o.version) - if err != nil { - return res, errors.Wrap(err, "an invalid version/constraint format") - } - - data := res[:0] - foundNames := map[string]bool{} - for _, r := range res { - if _, found := foundNames[r.Name]; found { - continue - } - v, err := semver.NewVersion(r.Chart.Version) - if err != nil || constraint.Check(v) { - data = append(data, r) - if !o.versions { - foundNames[r.Name] = true // If user hasn't requested all versions, only show the latest that matches - } - } - } - - return data, nil -} - -func (o *searchOptions) formatSearchResults(res []*search.Result) string { - if len(res) == 0 { - return "No results found" - } - table := uitable.New() - table.MaxColWidth = 50 - table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION") - for _, r := range res { - table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description) - } - return table.String() -} - -func (o *searchOptions) buildIndex(out io.Writer) (*search.Index, error) { - // Load the repositories.yaml - rf, err := repo.LoadFile(o.helmhome.RepositoryFile()) - if err != nil { - return nil, err - } - - i := search.NewIndex() - for _, re := range rf.Repositories { - n := re.Name - f := o.helmhome.CacheIndex(n) - ind, err := repo.LoadIndexFile(f) - if err != nil { - // TODO should print to stderr - fmt.Fprintf(out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n) - continue - } - - i.AddRepo(n, ind, o.versions || len(o.version) > 0) - } - return i, nil -} diff --git a/cmd/helm/search/search.go b/cmd/helm/search/search.go deleted file mode 100644 index a78f46da8..000000000 --- a/cmd/helm/search/search.go +++ /dev/null @@ -1,237 +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 search provides client-side repository searching. - -This supports building an in-memory search index based on the contents of -multiple repositories, and then using string matching or regular expressions -to find matches. -*/ -package search - -import ( - "path" - "regexp" - "sort" - "strings" - - "github.com/Masterminds/semver" - "github.com/pkg/errors" - - "helm.sh/helm/pkg/repo" -) - -// Result is a search result. -// -// Score indicates how close it is to match. The higher the score, the longer -// the distance. -type Result struct { - Name string - Score int - Chart *repo.ChartVersion -} - -// Index is a searchable index of chart information. -type Index struct { - lines map[string]string - charts map[string]*repo.ChartVersion -} - -const sep = "\v" - -// NewIndex creats a new Index. -func NewIndex() *Index { - return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} -} - -// verSep is a separator for version fields in map keys. -const verSep = "$$" - -// AddRepo adds a repository index to the search index. -func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { - ind.SortEntries() - for name, ref := range ind.Entries { - if len(ref) == 0 { - // Skip chart names that have zero releases. - continue - } - // By convention, an index file is supposed to have the newest at the - // 0 slot, so our best bet is to grab the 0 entry and build the index - // entry off of that. - // Note: Do not use filePath.Join since on Windows it will return \ - // which results in a repo name that cannot be understood. - fname := path.Join(rname, name) - if !all { - i.lines[fname] = indstr(rname, ref[0]) - i.charts[fname] = ref[0] - continue - } - - // If 'all' is set, then we go through all of the refs, and add them all - // to the index. This will generate a lot of near-duplicate entries. - for _, rr := range ref { - versionedName := fname + verSep + rr.Version - i.lines[versionedName] = indstr(rname, rr) - i.charts[versionedName] = rr - } - } -} - -// All returns all charts in the index as if they were search results. -// -// Each will be given a score of 0. -func (i *Index) All() []*Result { - res := make([]*Result, len(i.charts)) - j := 0 - for name, ch := range i.charts { - parts := strings.Split(name, verSep) - res[j] = &Result{ - Name: parts[0], - Chart: ch, - } - j++ - } - return res -} - -// Search searches an index for the given term. -// -// Threshold indicates the maximum score a term may have before being marked -// irrelevant. (Low score means higher relevance. Golf, not bowling.) -// -// If regexp is true, the term is treated as a regular expression. Otherwise, -// term is treated as a literal string. -func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) { - if regexp { - return i.SearchRegexp(term, threshold) - } - return i.SearchLiteral(term, threshold), nil -} - -// calcScore calculates a score for a match. -func (i *Index) calcScore(index int, matchline string) int { - - // This is currently tied to the fact that sep is a single char. - splits := []int{} - s := rune(sep[0]) - for i, ch := range matchline { - if ch == s { - splits = append(splits, i) - } - } - - for i, pos := range splits { - if index > pos { - continue - } - return i - } - return len(splits) -} - -// SearchLiteral does a literal string search (no regexp). -func (i *Index) SearchLiteral(term string, threshold int) []*Result { - term = strings.ToLower(term) - buf := []*Result{} - for k, v := range i.lines { - lk := strings.ToLower(k) - lv := strings.ToLower(v) - res := strings.Index(lv, term) - if score := i.calcScore(res, lv); res != -1 && score < threshold { - parts := strings.Split(lk, verSep) // Remove version, if it is there. - buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) - } - } - return buf -} - -// SearchRegexp searches using a regular expression. -func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { - matcher, err := regexp.Compile(re) - if err != nil { - return []*Result{}, err - } - buf := []*Result{} - for k, v := range i.lines { - ind := matcher.FindStringIndex(v) - if len(ind) == 0 { - continue - } - if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { - parts := strings.Split(k, verSep) // Remove version, if it is there. - buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) - } - } - return buf, nil -} - -// Chart returns the ChartVersion for a particular name. -func (i *Index) Chart(name string) (*repo.ChartVersion, error) { - c, ok := i.charts[name] - if !ok { - return nil, errors.New("no such chart") - } - return c, nil -} - -// SortScore does an in-place sort of the results. -// -// Lowest scores are highest on the list. Matching scores are subsorted alphabetically. -func SortScore(r []*Result) { - sort.Sort(scoreSorter(r)) -} - -// scoreSorter sorts results by score, and subsorts by alpha Name. -type scoreSorter []*Result - -// Len returns the length of this scoreSorter. -func (s scoreSorter) Len() int { return len(s) } - -// Swap performs an in-place swap. -func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less compares a to b, and returns true if a is less than b. -func (s scoreSorter) Less(a, b int) bool { - first := s[a] - second := s[b] - - if first.Score > second.Score { - return false - } - if first.Score < second.Score { - return true - } - if first.Name == second.Name { - v1, err := semver.NewVersion(first.Chart.Version) - if err != nil { - return true - } - v2, err := semver.NewVersion(second.Chart.Version) - if err != nil { - return true - } - // Sort so that the newest chart is higher than the oldest chart. This is - // the opposite of what you'd expect in a function called Less. - return v1.GreaterThan(v2) - } - return first.Name < second.Name -} - -func indstr(name string, ref *repo.ChartVersion) string { - i := ref.Name + sep + name + "/" + ref.Name + sep + - ref.Description + sep + strings.Join(ref.Keywords, " ") - return i -} diff --git a/cmd/helm/search/search_test.go b/cmd/helm/search/search_test.go deleted file mode 100644 index 47c0a0e9f..000000000 --- a/cmd/helm/search/search_test.go +++ /dev/null @@ -1,303 +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 search - -import ( - "strings" - "testing" - - "helm.sh/helm/pkg/chart" - "helm.sh/helm/pkg/repo" -) - -func TestSortScore(t *testing.T) { - in := []*Result{ - {Name: "bbb", Score: 0, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}}, - {Name: "aaa", Score: 5}, - {Name: "abb", Score: 5}, - {Name: "aab", Score: 0}, - {Name: "bab", Score: 5}, - {Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.4"}}}, - {Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}}, - } - expect := []string{"aab", "bbb", "aaa", "abb", "bab", "ver", "ver"} - expectScore := []int{0, 0, 5, 5, 5, 5, 5} - SortScore(in) - - // Test Score - for i := 0; i < len(expectScore); i++ { - if expectScore[i] != in[i].Score { - t.Errorf("Sort error on index %d: expected %d, got %d", i, expectScore[i], in[i].Score) - } - } - // Test Name - for i := 0; i < len(expect); i++ { - if expect[i] != in[i].Name { - t.Errorf("Sort error: expected %s, got %s", expect[i], in[i].Name) - } - } - - // Test version of last two items - if in[5].Chart.Version != "1.2.4" { - t.Errorf("Expected 1.2.4, got %s", in[5].Chart.Version) - } - if in[6].Chart.Version != "1.2.3" { - t.Error("Expected 1.2.3 to be last") - } -} - -var indexfileEntries = map[string]repo.ChartVersions{ - "niña": { - { - URLs: []string{"http://example.com/charts/nina-0.1.0.tgz"}, - Metadata: &chart.Metadata{ - Name: "niña", - Version: "0.1.0", - Description: "One boat", - }, - }, - }, - "pinta": { - { - URLs: []string{"http://example.com/charts/pinta-0.1.0.tgz"}, - Metadata: &chart.Metadata{ - Name: "pinta", - Version: "0.1.0", - Description: "Two ship", - }, - }, - }, - "santa-maria": { - { - URLs: []string{"http://example.com/charts/santa-maria-1.2.3.tgz"}, - Metadata: &chart.Metadata{ - Name: "santa-maria", - Version: "1.2.3", - Description: "Three boat", - }, - }, - { - URLs: []string{"http://example.com/charts/santa-maria-1.2.2-rc-1.tgz"}, - Metadata: &chart.Metadata{ - Name: "santa-maria", - Version: "1.2.2-RC-1", - Description: "Three boat", - }, - }, - }, -} - -func loadTestIndex(t *testing.T, all bool) *Index { - i := NewIndex() - i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) - i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ - "pinta": { - { - URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, - Metadata: &chart.Metadata{ - Name: "pinta", - Version: "2.0.0", - Description: "Two ship, version two", - }, - }, - }, - }}, all) - return i -} - -func TestAll(t *testing.T) { - i := loadTestIndex(t, false) - all := i.All() - if len(all) != 4 { - t.Errorf("Expected 4 entries, got %d", len(all)) - } - - i = loadTestIndex(t, true) - all = i.All() - if len(all) != 5 { - t.Errorf("Expected 5 entries, got %d", len(all)) - } -} - -func TestAddRepo_Sort(t *testing.T) { - i := loadTestIndex(t, true) - sr, err := i.Search("TESTING/SANTA-MARIA", 100, false) - if err != nil { - t.Fatal(err) - } - SortScore(sr) - - ch := sr[0] - expect := "1.2.3" - if ch.Chart.Version != expect { - t.Errorf("Expected %q, got %q", expect, ch.Chart.Version) - } -} - -func TestSearchByName(t *testing.T) { - - tests := []struct { - name string - query string - expect []*Result - regexp bool - fail bool - failMsg string - }{ - { - name: "basic search for one result", - query: "santa-maria", - expect: []*Result{ - {Name: "testing/santa-maria"}, - }, - }, - { - name: "basic search for two results", - query: "pinta", - expect: []*Result{ - {Name: "testing/pinta"}, - {Name: "ztesting/pinta"}, - }, - }, - { - name: "repo-specific search for one result", - query: "ztesting/pinta", - expect: []*Result{ - {Name: "ztesting/pinta"}, - }, - }, - { - name: "partial name search", - query: "santa", - expect: []*Result{ - {Name: "testing/santa-maria"}, - }, - }, - { - name: "description search, one result", - query: "Three", - expect: []*Result{ - {Name: "testing/santa-maria"}, - }, - }, - { - name: "description search, two results", - query: "two", - expect: []*Result{ - {Name: "testing/pinta"}, - {Name: "ztesting/pinta"}, - }, - }, - { - name: "description upper search, two results", - query: "TWO", - expect: []*Result{ - {Name: "testing/pinta"}, - {Name: "ztesting/pinta"}, - }, - }, - { - name: "nothing found", - query: "mayflower", - expect: []*Result{}, - }, - { - name: "regexp, one result", - query: "Th[ref]*", - expect: []*Result{ - {Name: "testing/santa-maria"}, - }, - regexp: true, - }, - { - name: "regexp, fail compile", - query: "th[", - expect: []*Result{}, - regexp: true, - fail: true, - failMsg: "error parsing regexp:", - }, - } - - i := loadTestIndex(t, false) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - charts, err := i.Search(tt.query, 100, tt.regexp) - if err != nil { - if tt.fail { - if !strings.Contains(err.Error(), tt.failMsg) { - t.Fatalf("Unexpected error message: %s", err) - } - return - } - t.Fatalf("%s: %s", tt.name, err) - } - // Give us predictably ordered results. - SortScore(charts) - - l := len(charts) - if l != len(tt.expect) { - t.Fatalf("Expected %d result, got %d", len(tt.expect), l) - } - // For empty result sets, just keep going. - if l == 0 { - return - } - - for i, got := range charts { - ex := tt.expect[i] - if got.Name != ex.Name { - t.Errorf("[%d]: Expected name %q, got %q", i, ex.Name, got.Name) - } - } - - }) - } -} - -func TestSearchByNameAll(t *testing.T) { - // Test with the All bit turned on. - i := loadTestIndex(t, true) - cs, err := i.Search("santa-maria", 100, false) - if err != nil { - t.Fatal(err) - } - if len(cs) != 2 { - t.Errorf("expected 2 charts, got %d", len(cs)) - } -} - -func TestCalcScore(t *testing.T) { - i := NewIndex() - - fields := []string{"aaa", "bbb", "ccc", "ddd"} - matchline := strings.Join(fields, sep) - if r := i.calcScore(2, matchline); r != 0 { - t.Errorf("Expected 0, got %d", r) - } - if r := i.calcScore(5, matchline); r != 1 { - t.Errorf("Expected 1, got %d", r) - } - if r := i.calcScore(10, matchline); r != 2 { - t.Errorf("Expected 2, got %d", r) - } - if r := i.calcScore(14, matchline); r != 3 { - t.Errorf("Expected 3, got %d", r) - } -} diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go deleted file mode 100644 index 380f87d34..000000000 --- a/cmd/helm/search_test.go +++ /dev/null @@ -1,72 +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 ( - "testing" -) - -func TestSearchCmd(t *testing.T) { - defer resetEnv()() - - setHome := func(cmd string) string { - return cmd + " --home=testdata/helmhome" - } - - tests := []cmdTestCase{{ - name: "search for 'maria', expect one match", - cmd: setHome("search maria"), - golden: "output/search-single.txt", - }, { - name: "search for 'alpine', expect two matches", - cmd: setHome("search alpine"), - golden: "output/search-multiple.txt", - }, { - name: "search for 'alpine' with versions, expect three matches", - cmd: setHome("search alpine --versions"), - golden: "output/search-multiple-versions.txt", - }, { - name: "search for 'alpine' with version constraint, expect one match with version 0.1.0", - cmd: setHome("search alpine --version '>= 0.1, < 0.2'"), - golden: "output/search-constraint.txt", - }, { - name: "search for 'alpine' with version constraint, expect one match with version 0.1.0", - cmd: setHome("search alpine --versions --version '>= 0.1, < 0.2'"), - golden: "output/search-versions-constraint.txt", - }, { - name: "search for 'alpine' with version constraint, expect one match with version 0.2.0", - cmd: setHome("search alpine --version '>= 0.1'"), - golden: "output/search-constraint-single.txt", - }, { - name: "search for 'alpine' with version constraint and --versions, expect two matches", - cmd: setHome("search alpine --versions --version '>= 0.1'"), - golden: "output/search-multiple-versions-constraints.txt", - }, { - name: "search for 'syzygy', expect no matches", - cmd: setHome("search syzygy"), - golden: "output/search-not-found.txt", - }, { - name: "search for 'alp[a-z]+', expect two matches", - cmd: setHome("search alp[a-z]+ --regexp"), - golden: "output/search-regex.txt", - }, { - name: "search for 'alp[', expect failure to compile regexp", - cmd: setHome("search alp[ --regexp"), - wantError: true, - }} - runTestCmd(t, tests) -} diff --git a/cmd/helm/show.go b/cmd/helm/show.go index 4b23473af..a9b9842c9 100644 --- a/cmd/helm/show.go +++ b/cmd/helm/show.go @@ -48,7 +48,7 @@ This command inspects a chart (directory, file, or URL) and displays the content of the README file ` -func newShowCmd(out io.Writer) *cobra.Command { +func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewShow(action.ShowAll) showCommand := &cobra.Command{ @@ -58,11 +58,11 @@ func newShowCmd(out io.Writer) *cobra.Command { Long: showDesc, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - cp, err := client.ChartPathOptions.LocateChart(args[0], settings) + ch, err := action.LocateChart(args[0], client.ChartPathOptions, settings, cfg.RegistryClient) if err != nil { return err } - output, err := client.Run(cp) + output, err := client.Run(ch) if err != nil { return err } @@ -78,11 +78,11 @@ func newShowCmd(out io.Writer) *cobra.Command { Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowValues - cp, err := client.ChartPathOptions.LocateChart(args[0], settings) + ch, err := action.LocateChart(args[0], client.ChartPathOptions, settings, cfg.RegistryClient) if err != nil { return err } - output, err := client.Run(cp) + output, err := client.Run(ch) if err != nil { return err } @@ -98,11 +98,11 @@ func newShowCmd(out io.Writer) *cobra.Command { Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowChart - cp, err := client.ChartPathOptions.LocateChart(args[0], settings) + ch, err := action.LocateChart(args[0], client.ChartPathOptions, settings, cfg.RegistryClient) if err != nil { return err } - output, err := client.Run(cp) + output, err := client.Run(ch) if err != nil { return err } @@ -118,11 +118,11 @@ func newShowCmd(out io.Writer) *cobra.Command { Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowReadme - cp, err := client.ChartPathOptions.LocateChart(args[0], settings) + ch, err := action.LocateChart(args[0], client.ChartPathOptions, settings, cfg.RegistryClient) if err != nil { return err } - output, err := client.Run(cp) + output, err := client.Run(ch) if err != nil { return err } diff --git a/cmd/helm/template.go b/cmd/helm/template.go index d3168c069..8d6e616b6 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -45,7 +45,7 @@ To render just one template in a chart, use '-x': $ helm template foo mychart -x templates/deployment.yaml ` -func newTemplateCmd(out io.Writer) *cobra.Command { +func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { customConfig := &action.Configuration{ // Add mock objects in here so it doesn't use Kube API server Releases: storage.Init(driver.NewMemory()), @@ -67,7 +67,7 @@ func newTemplateCmd(out io.Writer) *cobra.Command { client.DryRun = true client.ReleaseName = "RELEASE-NAME" client.Replace = true // Skip the name check - rel, err := runInstall(args, client, out) + rel, err := runInstall(args, client, cfg, out) if err != nil { return err } diff --git a/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt b/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt index 5652097a6..6dddc7344 100644 --- a/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt +++ b/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt @@ -1 +1 @@ -Error: cannot load Chart.yaml: error converting YAML to JSON: yaml: line 5: did not find expected '-' indicator +Error: cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator diff --git a/cmd/helm/testdata/testcharts/alpine/Chart.yaml b/cmd/helm/testdata/testcharts/alpine/Chart.yaml index e45d7326a..eec261220 100644 --- a/cmd/helm/testdata/testcharts/alpine/Chart.yaml +++ b/cmd/helm/testdata/testcharts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: Deploy a basic Alpine Linux pod home: https://helm.sh/helm name: alpine diff --git a/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml b/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml index ba72c77bf..1f445ee11 100644 --- a/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: chart-missing-deps version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml b/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml index c3813bc8c..356135537 100644 --- a/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqsubchart version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml b/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml index 75767a62c..e77b5afaa 100644 --- a/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml @@ -1,7 +1,8 @@ +apiVersion: v1 description: Deploy a basic Alpine Linux pod home: https://helm.sh/helm name: chart-bad-type sources: -- https://github.com/helm/helm + - https://github.com/helm/helm version: 0.1.0 type: foobar diff --git a/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml b/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml index 8cd47d20c..9605636db 100644 --- a/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: chart-missing-deps version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml b/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml index c3813bc8c..356135537 100644 --- a/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml +++ b/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqsubchart version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz b/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz index 575b27128..3c9c24d76 100644 Binary files a/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz and b/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz b/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz index ba96a80c9..16a644a79 100644 Binary files a/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz and b/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz b/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz index 89776bfa8..051bd6fd9 100644 Binary files a/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz and b/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml b/cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml index 3e65afdfa..92ba4d88f 100644 --- a/cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml +++ b/cmd/helm/testdata/testcharts/decompressedchart/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: decompressedchart version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/empty/Chart.yaml b/cmd/helm/testdata/testcharts/empty/Chart.yaml index 8bdba0330..4f1dc0012 100644 --- a/cmd/helm/testdata/testcharts/empty/Chart.yaml +++ b/cmd/helm/testdata/testcharts/empty/Chart.yaml @@ -1,6 +1,7 @@ +apiVersion: v1 description: Empty testing chart home: https://helm.sh/helm name: empty sources: -- https://github.com/helm/helm + - https://github.com/helm/helm version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/issue1979/Chart.yaml b/cmd/helm/testdata/testcharts/issue1979/Chart.yaml index e45d7326a..5269b5cf6 100644 --- a/cmd/helm/testdata/testcharts/issue1979/Chart.yaml +++ b/cmd/helm/testdata/testcharts/issue1979/Chart.yaml @@ -1,6 +1,7 @@ +apiVersion: v1 description: Deploy a basic Alpine Linux pod home: https://helm.sh/helm name: alpine sources: -- https://github.com/helm/helm + - https://github.com/helm/helm version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/novals/Chart.yaml b/cmd/helm/testdata/testcharts/novals/Chart.yaml index 905258117..ada85a4c4 100644 --- a/cmd/helm/testdata/testcharts/novals/Chart.yaml +++ b/cmd/helm/testdata/testcharts/novals/Chart.yaml @@ -1,6 +1,7 @@ +apiVersion: v1 description: Deploy a basic Alpine Linux pod home: https://helm.sh/helm name: novals sources: -- https://github.com/helm/helm + - https://github.com/helm/helm version: 0.2.0 diff --git a/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz b/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz index 8618e91d7..5d8e46a50 100644 Binary files a/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz and b/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/reqtest/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/Chart.yaml index e38826af3..07b6e2c97 100644 --- a/cmd/helm/testdata/testcharts/reqtest/Chart.yaml +++ b/cmd/helm/testdata/testcharts/reqtest/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqtest version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml index c3813bc8c..356135537 100644 --- a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqsubchart version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml index 9f7c22a71..5b9277370 100644 --- a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml +++ b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: reqsubchart2 version: 0.2.0 diff --git a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz index 84b0fb65e..37962b0ab 100644 Binary files a/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz and b/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz index 6de9d988d..c74e5b0ef 100644 Binary files a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz and b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz differ diff --git a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov index 94235399a..d325bb266 100644 --- a/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov +++ b/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov @@ -1,20 +1,21 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 +apiVersion: v1 description: A Helm chart for Kubernetes name: signtest version: 0.1.0 ... files: - signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b + signtest-0.1.0.tgz: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55 -----BEGIN PGP SIGNATURE----- -wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g -l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki -DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp -flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz -9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0 -S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s= -=NyOM +wsBcBAEBCgAQBQJcoosfCRCEO7+YH8GHYgAA220IALAs8T8NPgkcLvHu+5109cAN +BOCNPSZDNsqLZW/2Dc9cKoBG7Jen4Qad+i5l9351kqn3D9Gm6eRfAWcjfggRobV/ +9daZ19h0nl4O1muQNAkjvdgZt8MOP3+PB3I3/Tu2QCYjI579SLUmuXlcZR5BCFPR +PJy+e3QpV2PcdeU2KZLG4tjtlrq+3QC9ZHHEJLs+BVN9d46Dwo6CxJdHJrrrAkTw +M8MhA92vbiTTPRSCZI9x5qDAwJYhoq0oxLflpuL2tIlo3qVoCsaTSURwMESEHO32 +XwYG7BaVDMELWhAorBAGBGBwWFbJ1677qQ2gd9CN0COiVhekWlFRcnn60800r84= +=k9Y9 -----END PGP SIGNATURE----- \ No newline at end of file diff --git a/cmd/helm/testdata/testcharts/signtest/Chart.yaml b/cmd/helm/testdata/testcharts/signtest/Chart.yaml index 90964b44a..f1f73723a 100644 --- a/cmd/helm/testdata/testcharts/signtest/Chart.yaml +++ b/cmd/helm/testdata/testcharts/signtest/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: signtest version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml b/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml index e45d7326a..eec261220 100644 --- a/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml +++ b/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: Deploy a basic Alpine Linux pod home: https://helm.sh/helm name: alpine diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 7a5aebdbc..c1d17af59 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -25,7 +25,6 @@ import ( "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" - "helm.sh/helm/pkg/chart/loader" "helm.sh/helm/pkg/storage/driver" ) @@ -74,7 +73,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } - chartPath, err := client.ChartPathOptions.LocateChart(args[1], settings) + ch, err := action.LocateChart(args[1], client.ChartPathOptions, settings, cfg.RegistryClient) if err != nil { return err } @@ -96,16 +95,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Devel = client.Devel instClient.Namespace = client.Namespace - _, err := runInstall(args, instClient, out) + _, err := runInstall(args, instClient, cfg, out) return err } } - // Check chart dependencies to make sure all are present in /charts - ch, err := loader.Load(chartPath) - if err != nil { - return err - } if req := ch.Metadata.Dependencies; req != nil { if err := action.CheckDependencies(ch, req); err != nil { return err diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index bba9256fc..e8b31431e 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -29,6 +29,7 @@ import ( func TestUpgradeCmd(t *testing.T) { tmpChart := testTempDir(t) cfile := &chart.Metadata{ + APIVersion: chart.APIVersionV1, Name: "testUpgradeChart", Description: "A Helm chart for Kubernetes", Version: "0.1.0", diff --git a/docs/examples/alpine/Chart.yaml b/docs/examples/alpine/Chart.yaml index e56f8a469..a2403f594 100644 --- a/docs/examples/alpine/Chart.yaml +++ b/docs/examples/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/docs/examples/nginx/Chart.yaml b/docs/examples/nginx/Chart.yaml index 3d6f5751b..a6a942435 100644 --- a/docs/examples/nginx/Chart.yaml +++ b/docs/examples/nginx/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: nginx description: A basic NGINX HTTP server version: 0.1.0 diff --git a/docs/examples/nginx/charts/alpine/Chart.yaml b/docs/examples/nginx/charts/alpine/Chart.yaml index e56f8a469..a2403f594 100644 --- a/docs/examples/nginx/charts/alpine/Chart.yaml +++ b/docs/examples/nginx/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/action/action.go b/pkg/action/action.go index fbd51114f..1bfeb5e69 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -26,8 +26,8 @@ import ( "helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/kube" - "helm.sh/helm/pkg/registry" "helm.sh/helm/pkg/release" + "helm.sh/helm/pkg/repo" "helm.sh/helm/pkg/storage" ) @@ -70,8 +70,8 @@ type Configuration struct { // KubeClient is a Kubernetes API client. KubeClient kube.KubernetesClient - // RegistryClient is a client for working with registries - RegistryClient *registry.Client + // RegistryClient is a client for working with repositories + RegistryClient *repo.Client Capabilities *chartutil.Capabilities diff --git a/pkg/action/chart_export.go b/pkg/action/chart_export.go index 5bfb002de..ca9a880df 100644 --- a/pkg/action/chart_export.go +++ b/pkg/action/chart_export.go @@ -20,8 +20,9 @@ import ( "fmt" "io" + "github.com/containerd/containerd/reference" + "helm.sh/helm/pkg/chartutil" - "helm.sh/helm/pkg/registry" ) // ChartExport performs a chart export operation. @@ -38,7 +39,7 @@ func NewChartExport(cfg *Configuration) *ChartExport { // Run executes the chart export operation func (a *ChartExport) Run(out io.Writer, ref string) error { - r, err := registry.ParseReference(ref) + r, err := reference.Parse(ref) if err != nil { return err } diff --git a/pkg/action/chart_pull.go b/pkg/action/chart_pull.go index f4388a5ea..672e2fdca 100644 --- a/pkg/action/chart_pull.go +++ b/pkg/action/chart_pull.go @@ -19,7 +19,7 @@ package action import ( "io" - "helm.sh/helm/pkg/registry" + "github.com/containerd/containerd/reference" ) // ChartPull performs a chart pull operation. @@ -36,7 +36,7 @@ func NewChartPull(cfg *Configuration) *ChartPull { // Run executes the chart pull operation func (a *ChartPull) Run(out io.Writer, ref string) error { - r, err := registry.ParseReference(ref) + r, err := reference.Parse(ref) if err != nil { return err } diff --git a/pkg/action/chart_push.go b/pkg/action/chart_push.go index 97ab77fc0..1b2a43a6f 100644 --- a/pkg/action/chart_push.go +++ b/pkg/action/chart_push.go @@ -19,7 +19,7 @@ package action import ( "io" - "helm.sh/helm/pkg/registry" + "github.com/containerd/containerd/reference" ) // ChartPush performs a chart push operation. @@ -36,7 +36,7 @@ func NewChartPush(cfg *Configuration) *ChartPush { // Run executes the chart push operation func (a *ChartPush) Run(out io.Writer, ref string) error { - r, err := registry.ParseReference(ref) + r, err := reference.Parse(ref) if err != nil { return err } diff --git a/pkg/action/chart_remove.go b/pkg/action/chart_remove.go index ae1d93135..3c7f29042 100644 --- a/pkg/action/chart_remove.go +++ b/pkg/action/chart_remove.go @@ -19,7 +19,7 @@ package action import ( "io" - "helm.sh/helm/pkg/registry" + "github.com/containerd/containerd/reference" ) // ChartRemove performs a chart remove operation. @@ -36,7 +36,7 @@ func NewChartRemove(cfg *Configuration) *ChartRemove { // Run executes the chart remove operation func (a *ChartRemove) Run(out io.Writer, ref string) error { - r, err := registry.ParseReference(ref) + r, err := reference.Parse(ref) if err != nil { return err } diff --git a/pkg/action/chart_save.go b/pkg/action/chart_save.go deleted file mode 100644 index 88ed25b36..000000000 --- a/pkg/action/chart_save.go +++ /dev/null @@ -1,57 +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" - "path/filepath" - - "helm.sh/helm/pkg/chart/loader" - "helm.sh/helm/pkg/registry" -) - -// 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, path, ref string) error { - path, err := filepath.Abs(path) - if err != nil { - return err - } - - ch, err := loader.LoadDir(path) - if err != nil { - return err - } - - r, err := registry.ParseReference(ref) - if err != nil { - return err - } - - return a.cfg.RegistryClient.SaveChart(ch, r) -} diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go index 849904884..bb352a24a 100644 --- a/pkg/action/dependency.go +++ b/pkg/action/dependency.go @@ -140,9 +140,9 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency) s func (d *Dependency) printDependencies(chartpath string, out io.Writer, reqs []*chart.Dependency) { table := uitable.New() table.MaxColWidth = 80 - table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") + table.AddRow("NAME", "VERSION", "STATUS") for _, row := range reqs { - table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row)) + table.AddRow(row.Name, row.Version, d.dependencyStatus(chartpath, row)) } fmt.Fprintln(out, table) } diff --git a/pkg/action/install.go b/pkg/action/install.go index 24d33d26a..892d14f22 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -35,9 +35,9 @@ import ( "github.com/pkg/errors" "helm.sh/helm/pkg/chart" + "helm.sh/helm/pkg/chart/loader" "helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/cli" - "helm.sh/helm/pkg/downloader" "helm.sh/helm/pkg/engine" "helm.sh/helm/pkg/getter" "helm.sh/helm/pkg/hooks" @@ -521,78 +521,52 @@ OUTER: return nil } -// LocateChart looks for a chart directory in known places, and returns either the full path or an error. +// LocateChart looks for a chart directory in known places, and returns either the chart or an error. // -// This does not ensure that the chart is well-formed; only that the requested filename exists. +// This does not ensure that the chart is well-formed; only that the requested chart exists. // // Order of resolution: // - relative to current working directory // - if path is absolute or begins with '.', error out here -// - chart repos in $HELM_HOME -// - URL +// - pull from the given URL // // If 'verify' is true, this will attempt to also verify the chart. -func (c *ChartPathOptions) LocateChart(name string, settings cli.EnvSettings) (string, error) { +func LocateChart(name string, c ChartPathOptions, settings cli.EnvSettings, repoClient *repo.Client) (*chart.Chart, error) { name = strings.TrimSpace(name) version := strings.TrimSpace(c.Version) if _, err := os.Stat(name); err == nil { abs, err := filepath.Abs(name) if err != nil { - return abs, err + return nil, err } if c.Verify { - if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil { - return "", err + if _, err := VerifyChart(abs, c.Keyring); err != nil { + return nil, err } } - return abs, nil + return loader.Load(abs) } if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { - return name, errors.Errorf("path %q not found", name) + return nil, errors.Errorf("path %q not found", name) } - crepo := filepath.Join(settings.Home.Repository(), name) - if _, err := os.Stat(crepo); err == nil { - return filepath.Abs(crepo) + ref, err := repo.ParseNameTag(name, version) + if err != nil { + return nil, err } - dl := downloader.ChartDownloader{ - HelmHome: settings.Home, - Out: os.Stdout, - Keyring: c.Keyring, - Getters: getter.All(settings), - Username: c.Username, - Password: c.Password, - } if c.Verify { - dl.Verify = downloader.VerifyAlways - } - if c.RepoURL != "" { - chartURL, err := repo.FindChartInAuthRepoURL(c.RepoURL, c.Username, c.Password, name, version, - c.CertFile, c.KeyFile, c.CaFile, getter.All(settings)) - if err != nil { - return "", err - } - name = chartURL - } - - if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { - os.MkdirAll(settings.Home.Archive(), 0744) + // TODO(bacongobbler): plug into pkg/repo or oras for signing during a pull + // + // see comment in pkg/repo/client.go#PullChart } - filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) - if err == nil { - lname, err := filepath.Abs(filename) - if err != nil { - return filename, err - } - return lname, nil - } else if settings.Debug { - return filename, err + if err := repoClient.PullChart(ref); err != nil { + return nil, fmt.Errorf("failed to download %q: %v", ref.String(), err) } - return filename, errors.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) + return repoClient.LoadChart(ref) } // MergeValues merges values from files specified via -f/--values and diff --git a/pkg/action/package.go b/pkg/action/package.go index af7edea44..79a7b394b 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -19,7 +19,6 @@ package action import ( "fmt" "io/ioutil" - "os" "syscall" "github.com/Masterminds/semver" @@ -36,6 +35,7 @@ import ( // // It provides the implementation of 'helm package'. type Package struct { + cfg *Configuration ValueOptions Sign bool @@ -43,37 +43,37 @@ type Package struct { Keyring string Version string AppVersion string - Destination string + Registry string DependencyUpdate bool } // NewPackage creates a new Package object with the given configuration. -func NewPackage() *Package { - return &Package{} +func NewPackage(cfg *Configuration) *Package { + return &Package{cfg: cfg} } // Run executes 'helm package' against the given chart and returns the path to the packaged chart. -func (p *Package) Run(path string) (string, error) { +func (p *Package) Run(path string) error { ch, err := loader.LoadDir(path) if err != nil { - return "", err + return err } validChartType, err := chartutil.IsValidChartType(ch) if !validChartType { - return "", err + return err } combinedVals, err := chartutil.CoalesceValues(ch, p.ValueOptions.rawValues) if err != nil { - return "", err + return err } ch.Values = combinedVals // If version is set, modify the version. if p.Version != "" { if err := setVersion(ch, p.Version); err != nil { - return "", err + return err } } @@ -83,32 +83,19 @@ func (p *Package) Run(path string) (string, error) { if reqs := ch.Metadata.Dependencies; reqs != nil { if err := CheckDependencies(ch, reqs); err != nil { - return "", err - } - } - - var dest string - if p.Destination == "." { - // Save to the current working directory. - dest, err = os.Getwd() - if err != nil { - return "", err + return err } - } else { - // Otherwise save to set destination - dest = p.Destination } - name, err := chartutil.Save(ch, dest) - if err != nil { - return "", errors.Wrap(err, "failed to save") + if err := p.cfg.RegistryClient.SaveChart(ch, p.Registry); err != nil { + return errors.Wrap(err, "failed to save") } if p.Sign { - err = p.Clearsign(name) + // TODO(bacongobbler): tie into oras/notary for signing OCI manifests } - return "", err + return nil } func setVersion(ch *chart.Chart, ver string) error { diff --git a/pkg/action/pull.go b/pkg/action/pull.go index 8925c7190..53be469af 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -27,8 +27,6 @@ import ( "helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/cli" - "helm.sh/helm/pkg/downloader" - "helm.sh/helm/pkg/getter" "helm.sh/helm/pkg/repo" ) @@ -36,6 +34,7 @@ import ( // // It provides the implementation of 'helm pull'. type Pull struct { + cfg *Configuration ChartPathOptions Settings cli.EnvSettings // TODO: refactor this out of pkg/action @@ -48,32 +47,34 @@ type Pull struct { } // NewPull creates a new Pull object with the given configuration. -func NewPull() *Pull { - return &Pull{} +func NewPull(cfg *Configuration) *Pull { + return &Pull{cfg: cfg} } // Run executes 'helm pull' against the given release. func (p *Pull) Run(chartRef string) (string, error) { var out strings.Builder - c := downloader.ChartDownloader{ - HelmHome: p.Settings.Home, - Out: &out, - Keyring: p.Keyring, - Verify: downloader.VerifyNever, - Getters: getter.All(p.Settings), - Username: p.Username, - Password: p.Password, + ref, err := repo.ParseNameTag(chartRef, p.Version) + if err != nil { + return out.String(), err } if p.Verify { - c.Verify = downloader.VerifyAlways - } else if p.VerifyLater { - c.Verify = downloader.VerifyLater + // TODO(bacongobbler): plug into pkg/repo or oras for signing during a pull + // + // see comment in pkg/repo/client.go#PullChart + } + + if err := p.cfg.RegistryClient.PullChart(ref); err != nil { + return out.String(), fmt.Errorf("failed to download %q: %v", ref.String(), err) + } + + ch, err := p.cfg.RegistryClient.LoadChart(ref) + if err != nil { + return out.String(), err } - // If untar is set, we fetch to a tempdir, then untar and copy after - // verification. dest := p.DestDir if p.Untar { var err error @@ -84,23 +85,11 @@ func (p *Pull) Run(chartRef string) (string, error) { defer os.RemoveAll(dest) } - if p.RepoURL != "" { - chartURL, err := repo.FindChartInAuthRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, getter.All(p.Settings)) - if err != nil { - return out.String(), err - } - chartRef = chartURL - } - - saved, v, err := c.DownloadTo(chartRef, p.Version, dest) + saved, err := chartutil.Save(ch, dest) if err != nil { return out.String(), err } - if p.Verify { - fmt.Fprintf(&out, "Verification: %v\n", v) - } - // After verification, untar the chart into the requested directory. if p.Untar { ud := p.UntarDir @@ -118,5 +107,6 @@ func (p *Pull) Run(chartRef string) (string, error) { return out.String(), chartutil.ExpandFile(ud, saved) } + return out.String(), nil } diff --git a/pkg/action/show.go b/pkg/action/show.go index fd78dfeb0..727286290 100644 --- a/pkg/action/show.go +++ b/pkg/action/show.go @@ -23,7 +23,6 @@ import ( "github.com/ghodss/yaml" "helm.sh/helm/pkg/chart" - "helm.sh/helm/pkg/chart/loader" ) type ShowOutputFormat string @@ -57,12 +56,8 @@ func NewShow(output ShowOutputFormat) *Show { } // Run executes 'helm show' against the given release. -func (s *Show) Run(chartpath string) (string, error) { +func (s *Show) Run(chrt *chart.Chart) (string, error) { var out strings.Builder - chrt, err := loader.Load(chartpath) - if err != nil { - return "", err - } cf, err := yaml.Marshal(chrt.Metadata) if err != nil { return "", err diff --git a/pkg/action/show_test.go b/pkg/action/show_test.go index 0a532746a..3492a1cce 100644 --- a/pkg/action/show_test.go +++ b/pkg/action/show_test.go @@ -20,12 +20,19 @@ import ( "io/ioutil" "strings" "testing" + + "helm.sh/helm/pkg/chart/loader" ) func TestShow(t *testing.T) { client := NewShow(ShowAll) - output, err := client.Run("../../cmd/helm/testdata/testcharts/alpine") + ch, err := loader.Load("../../cmd/helm/testdata/testcharts/alpine") + if err != nil { + t.Fatal(err) + } + + output, err := client.Run(ch) if err != nil { t.Fatal(err) } @@ -65,7 +72,13 @@ func TestShow(t *testing.T) { // Regression tests for missing values. See issue #1024. client.OutputFormat = ShowValues - output, err = client.Run("../../cmd/helm/testdata/testcharts/novals") + + ch2, err := loader.Load("../../cmd/helm/testdata/testcharts/novals") + if err != nil { + t.Fatal(err) + } + + output, err = client.Run(ch2) if err != nil { t.Fatal(err) } diff --git a/pkg/action/verify.go b/pkg/action/verify.go index e78d50a14..6cc17301d 100644 --- a/pkg/action/verify.go +++ b/pkg/action/verify.go @@ -17,7 +17,13 @@ limitations under the License. package action import ( - "helm.sh/helm/pkg/downloader" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "helm.sh/helm/pkg/provenance" ) // Verify is the action for building a given chart's Verify tree. @@ -34,6 +40,32 @@ func NewVerify() *Verify { // Run executes 'helm verify'. func (v *Verify) Run(chartfile string) error { - _, err := downloader.VerifyChart(chartfile, v.Keyring) + _, err := VerifyChart(chartfile, v.Keyring) return err } + +// VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. +// +// It assumes that a chart archive file is accompanied by a provenance file whose +// name is the archive file name plus the ".prov" extension. +func VerifyChart(path, keyring string) (*provenance.Verification, error) { + // For now, error out if it's not a tar file. + if fi, err := os.Stat(path); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("unpacked charts cannot be verified") + } else if strings.ToLower(filepath.Ext(path)) != ".tgz" { + return nil, errors.New("chart must be a tgz file") + } + + provfile := path + ".prov" + if _, err := os.Stat(provfile); err != nil { + return nil, errors.Wrapf(err, "could not load provenance file %s", provfile) + } + + sig, err := provenance.NewFromKeyring(keyring, "") + if err != nil { + return nil, errors.Wrap(err, "failed to load keyring") + } + return sig.Verify(path, provfile) +} diff --git a/pkg/action/verify_test.go b/pkg/action/verify_test.go new file mode 100644 index 000000000..525e2cc66 --- /dev/null +++ b/pkg/action/verify_test.go @@ -0,0 +1,31 @@ +/* +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 "testing" + +func TestVerifyChart(t *testing.T) { + v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/helm-test-key.pub") + if err != nil { + t.Fatal(err) + } + // The verification is tested at length in the provenance package. Here, + // we just want a quick sanity check that the v is not empty. + if len(v.FileHash) == 0 { + t.Error("Digest missing") + } +} diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go index ac2bf8b6b..06aebf37b 100644 --- a/pkg/chart/chart.go +++ b/pkg/chart/chart.go @@ -15,8 +15,8 @@ limitations under the License. package chart -// APIVersionv1 is the API version number for version 1. -const APIVersionv1 = "v1" +// APIVersionV1 is the API version number for version 1. +const APIVersionV1 = "v1" // Chart is a helm package that contains metadata, a default config, zero or more // optionally parameterizable templates, and zero or more charts (dependencies). @@ -96,3 +96,7 @@ func (ch *Chart) ChartFullPath() string { } return ch.Name() } + +func (ch *Chart) Validate() error { + return ch.Metadata.Validate() +} diff --git a/pkg/chart/dependency.go b/pkg/chart/dependency.go index 0837f8d19..da002a1fe 100644 --- a/pkg/chart/dependency.go +++ b/pkg/chart/dependency.go @@ -31,11 +31,6 @@ type Dependency struct { // A lock file will always produce a single version, while a dependency // may contain a semantic version range. Version string `json:"version,omitempty"` - // The URL to the repository. - // - // Appending `index.yaml` to this string should result in a URL that can be - // used to fetch the repository index. - Repository string `json:"repository"` // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) Condition string `json:"condition,omitempty"` // Tags can be used to group charts for enabling/disabling together diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go index 0782a964e..67a9f6279 100644 --- a/pkg/chart/loader/load.go +++ b/pkg/chart/loader/load.go @@ -106,12 +106,8 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { } } - // Ensure that we got a Chart.yaml file - if c.Metadata == nil { - return c, errors.New("chart metadata (Chart.yaml) missing") - } - if c.Name() == "" { - return c, errors.New("invalid chart (Chart.yaml): name must not be empty") + if err := c.Validate(); err != nil { + return c, err } for n, files := range subcharts { diff --git a/pkg/chart/loader/load_test.go b/pkg/chart/loader/load_test.go index 686f50dcf..2c4cc0413 100644 --- a/pkg/chart/loader/load_test.go +++ b/pkg/chart/loader/load_test.go @@ -109,7 +109,7 @@ icon: https://example.com/64x64.png if err == nil { t.Fatal("Expected err to be non-nil") } - if err.Error() != "chart metadata (Chart.yaml) missing" { + if err.Error() != "metadata is required" { t.Errorf("Expected chart metadata missing error, got '%s'", err.Error()) } } @@ -180,8 +180,8 @@ func verifyDependencies(t *testing.T, c *chart.Chart) { t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) } tests := []*chart.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + {Name: "alpine", Version: "0.1.0"}, + {Name: "mariner", Version: "4.3.2"}, } for i, tt := range tests { d := c.Metadata.Dependencies[i] @@ -191,9 +191,6 @@ func verifyDependencies(t *testing.T, c *chart.Chart) { if d.Version != tt.Version { t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) } - if d.Repository != tt.Repository { - t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) - } } } @@ -202,8 +199,8 @@ func verifyDependenciesLock(t *testing.T, c *chart.Chart) { t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) } tests := []*chart.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + {Name: "alpine", Version: "0.1.0"}, + {Name: "mariner", Version: "4.3.2"}, } for i, tt := range tests { d := c.Metadata.Dependencies[i] @@ -213,9 +210,6 @@ func verifyDependenciesLock(t *testing.T, c *chart.Chart) { if d.Version != tt.Version { t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) } - if d.Repository != tt.Repository { - t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) - } } } diff --git a/pkg/chart/loader/testdata/albatross/Chart.yaml b/pkg/chart/loader/testdata/albatross/Chart.yaml index eeef737ff..b5188fde0 100644 --- a/pkg/chart/loader/testdata/albatross/Chart.yaml +++ b/pkg/chart/loader/testdata/albatross/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: albatross description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz b/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz index 983820e06..b2b76a83c 100644 Binary files a/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz and b/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100644 --- a/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml +++ b/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100644 --- a/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100644 Binary files a/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz index 88c24d822..3190136b0 100644 Binary files a/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz and b/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz b/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz index 513bfca1a..a9d4c11d8 100644 Binary files a/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz and b/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100755 --- a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100755 --- a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100755 Binary files a/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz b/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz index 3af333e76..3190136b0 100755 Binary files a/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz and b/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chart/loader/testdata/genfrob.sh b/pkg/chart/loader/testdata/genfrob.sh index 8f2cddec1..35fdd59f2 100755 --- a/pkg/chart/loader/testdata/genfrob.sh +++ b/pkg/chart/loader/testdata/genfrob.sh @@ -6,7 +6,9 @@ tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross echo "Packing mariner into frobnitz" tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner +tar -zcvf frobnitz_backslash/charts/mariner-4.3.2.tgz mariner # Pack the frobnitz chart. echo "Packing frobnitz" tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz +tar --exclude=ignore/* -zcvf frobnitz_backslash-1.2.3.tgz frobnitz_backslash diff --git a/pkg/chart/loader/testdata/mariner/Chart.yaml b/pkg/chart/loader/testdata/mariner/Chart.yaml index e2efb7f99..92dc4b390 100644 --- a/pkg/chart/loader/testdata/mariner/Chart.yaml +++ b/pkg/chart/loader/testdata/mariner/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mariner description: A Helm chart for Kubernetes version: 4.3.2 diff --git a/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz index fa8ef5aca..128ef82f7 100644 Binary files a/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz and b/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz differ diff --git a/pkg/chart/metadata.go b/pkg/chart/metadata.go index d0f03a61a..4a4139d1b 100644 --- a/pkg/chart/metadata.go +++ b/pkg/chart/metadata.go @@ -15,6 +15,8 @@ limitations under the License. package chart +import "errors" + // Maintainer describes a Chart maintainer. type Maintainer struct { // Name is a user name or organization name @@ -65,3 +67,20 @@ type Metadata struct { // Specifies the chart type: application or library Type string `json:"type,omitempty"` } + +func (md *Metadata) Validate() error { + if md == nil { + return errors.New("metadata is required") + } + if md.APIVersion == "" { + return errors.New("metadata apiVersion is required") + } + if md.Name == "" { + return errors.New("metadata name is required") + } + if md.Version == "" { + return errors.New("metadata version is required") + } + // TODO validate valid semver here? + return nil +} diff --git a/pkg/chartutil/chartfile_test.go b/pkg/chartutil/chartfile_test.go index 358c8231d..33d149501 100644 --- a/pkg/chartutil/chartfile_test.go +++ b/pkg/chartutil/chartfile_test.go @@ -40,8 +40,8 @@ func verifyChartfile(t *testing.T, f *chart.Metadata, name string) { } // Api instead of API because it was generated via protobuf. - if f.APIVersion != chart.APIVersionv1 { - t.Errorf("Expected API Version %q, got %q", chart.APIVersionv1, f.APIVersion) + if f.APIVersion != chart.APIVersionV1 { + t.Errorf("Expected API Version %q, got %q", chart.APIVersionV1, f.APIVersion) } if f.Name != name { diff --git a/pkg/chartutil/create_test.go b/pkg/chartutil/create_test.go index ab8b43e96..162168690 100644 --- a/pkg/chartutil/create_test.go +++ b/pkg/chartutil/create_test.go @@ -34,7 +34,11 @@ func TestCreate(t *testing.T) { } defer os.RemoveAll(tdir) - cf := &chart.Metadata{Name: "foo"} + cf := &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "foo", + Version: "0.1.0", + } c, err := Create(cf, tdir) if err != nil { @@ -85,7 +89,11 @@ func TestCreateFrom(t *testing.T) { } defer os.RemoveAll(tdir) - cf := &chart.Metadata{Name: "foo"} + cf := &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "foo", + Version: "0.1.0", + } srcdir := "./testdata/mariner" if err := CreateFrom(cf, tdir, srcdir); err != nil { diff --git a/pkg/chartutil/dependencies_test.go b/pkg/chartutil/dependencies_test.go index 10f7d250f..43cbd050d 100644 --- a/pkg/chartutil/dependencies_test.go +++ b/pkg/chartutil/dependencies_test.go @@ -27,6 +27,7 @@ import ( ) func loadChart(t *testing.T, path string) *chart.Chart { + t.Helper() c, err := loader.Load(path) if err != nil { t.Fatalf("failed to load testdata: %s", err) @@ -36,8 +37,8 @@ func loadChart(t *testing.T, path string) *chart.Chart { func TestLoadDependency(t *testing.T) { tests := []*chart.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + {Name: "alpine", Version: "0.1.0"}, + {Name: "mariner", Version: "4.3.2"}, } check := func(deps []*chart.Dependency) { @@ -51,9 +52,6 @@ func TestLoadDependency(t *testing.T) { if deps[i].Version != tt.Version { t.Errorf("expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, deps[i].Version) } - if deps[i].Repository != tt.Repository { - t.Errorf("expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, deps[i].Repository) - } } } c := loadChart(t, "testdata/frobnitz") diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go index a456b206e..faa583d10 100644 --- a/pkg/chartutil/save.go +++ b/pkg/chartutil/save.go @@ -20,6 +20,7 @@ import ( "archive/tar" "compress/gzip" "fmt" + "io" "io/ioutil" "os" "path/filepath" @@ -93,6 +94,23 @@ func SaveDir(c *chart.Chart, dest string) error { return nil } +// SaveToWriter archives the chart and writes to w. +func SaveToWriter(c *chart.Chart, w io.Writer) error { + // Wrap in gzip writer + zipper := gzip.NewWriter(w) + zipper.Header.Extra = headerBytes + zipper.Header.Comment = "Helm" + + // Wrap in tar writer + twriter := tar.NewWriter(zipper) + defer func() { + twriter.Close() + zipper.Close() + }() + + return writeTarContents(twriter, c, "") +} + // Save creates an archived chart to the given directory. // // This takes an existing chart and a destination directory. @@ -109,18 +127,11 @@ func Save(c *chart.Chart, outDir string) (string, error) { return "", errors.Errorf("location %s is not a directory", outDir) } - if c.Metadata == nil { - return "", errors.New("no Chart.yaml data") - } - - cfile := c.Metadata - if cfile.Name == "" { - return "", errors.New("no chart name specified (Chart.yaml)") - } else if cfile.Version == "" { - return "", errors.New("no chart version specified (Chart.yaml)") + if err := c.Validate(); err != nil { + return "", errors.Wrap(err, "chart validation") } - filename := fmt.Sprintf("%s-%s.tgz", cfile.Name, cfile.Version) + filename := fmt.Sprintf("%s-%s.tgz", c.Name(), c.Metadata.Version) filename = filepath.Join(outDir, filename) if stat, err := os.Stat(filepath.Dir(filename)); os.IsNotExist(err) { if err := os.MkdirAll(filepath.Dir(filename), 0755); !os.IsExist(err) { @@ -135,27 +146,15 @@ func Save(c *chart.Chart, outDir string) (string, error) { return "", err } - // Wrap in gzip writer - zipper := gzip.NewWriter(f) - zipper.Header.Extra = headerBytes - zipper.Header.Comment = "Helm" - - // Wrap in tar writer - twriter := tar.NewWriter(zipper) rollback := false defer func() { - twriter.Close() - zipper.Close() f.Close() if rollback { os.Remove(filename) } }() - if err := writeTarContents(twriter, c, ""); err != nil { - rollback = true - } - return filename, err + return filename, SaveToWriter(c, f) } func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { diff --git a/pkg/chartutil/save_test.go b/pkg/chartutil/save_test.go index 72b804c81..3ac13b476 100644 --- a/pkg/chartutil/save_test.go +++ b/pkg/chartutil/save_test.go @@ -35,8 +35,9 @@ func TestSave(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{ - Name: "ahab", - Version: "1.2.3.4", + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", }, Files: []*chart.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, @@ -80,8 +81,9 @@ func TestSaveDir(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{ - Name: "ahab", - Version: "1.2.3.4", + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", }, Files: []*chart.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, diff --git a/pkg/chartutil/testdata/albatross/Chart.yaml b/pkg/chartutil/testdata/albatross/Chart.yaml index eeef737ff..b5188fde0 100644 --- a/pkg/chartutil/testdata/albatross/Chart.yaml +++ b/pkg/chartutil/testdata/albatross/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: albatross description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100644 --- a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100644 --- a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz index 3af333e76..3190136b0 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100644 --- a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100644 --- a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100644 --- a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100644 --- a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz index 3af333e76..3190136b0 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz index 3af333e76..3190136b0 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100644 --- a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz index 3af333e76..3190136b0 100644 Binary files a/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz index a8ba8ec6f..8731dce02 100644 Binary files a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz and b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100644 --- a/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100644 --- a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100644 Binary files a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz index ea6e2a160..5648f6f6d 100644 Binary files a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz index bc6be27aa..692965951 100644 Binary files a/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz and b/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/Chart.yaml index 4cc36dca5..79e0d65db 100755 --- a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: alpine description: Deploy a basic Alpine Linux pod version: 0.1.0 diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml index 171e36156..1c9dd5fa4 100755 --- a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml +++ b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mast1 description: A Helm chart for Kubernetes version: 0.1.0 diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz index ced5a4a6a..61cb62051 100755 Binary files a/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz and b/pkg/chartutil/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz index 3af333e76..5648f6f6d 100755 Binary files a/pkg/chartutil/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz and b/pkg/chartutil/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/genfrob.sh b/pkg/chartutil/testdata/genfrob.sh index 8f2cddec1..35fdd59f2 100755 --- a/pkg/chartutil/testdata/genfrob.sh +++ b/pkg/chartutil/testdata/genfrob.sh @@ -6,7 +6,9 @@ tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross echo "Packing mariner into frobnitz" tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner +tar -zcvf frobnitz_backslash/charts/mariner-4.3.2.tgz mariner # Pack the frobnitz chart. echo "Packing frobnitz" tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz +tar --exclude=ignore/* -zcvf frobnitz_backslash-1.2.3.tgz frobnitz_backslash diff --git a/pkg/chartutil/testdata/mariner/Chart.yaml b/pkg/chartutil/testdata/mariner/Chart.yaml index e2efb7f99..92dc4b390 100644 --- a/pkg/chartutil/testdata/mariner/Chart.yaml +++ b/pkg/chartutil/testdata/mariner/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: mariner description: A Helm chart for Kubernetes version: 4.3.2 diff --git a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz index 72bb6b36f..22c1fe572 100644 Binary files a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz and b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/moby/Chart.yaml b/pkg/chartutil/testdata/moby/Chart.yaml index b725af916..a5f992c61 100644 --- a/pkg/chartutil/testdata/moby/Chart.yaml +++ b/pkg/chartutil/testdata/moby/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: moby version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml b/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml index d9a3bfd5f..f1a8ef76b 100644 --- a/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml +++ b/pkg/chartutil/testdata/moby/charts/pequod/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: pequod version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml index c3cdf397d..a7ee7bf90 100644 --- a/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml +++ b/pkg/chartutil/testdata/moby/charts/pequod/charts/ahab/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: ahab version: 0.1.0 diff --git a/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml b/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml index f6819c06d..0525085b6 100644 --- a/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml +++ b/pkg/chartutil/testdata/moby/charts/spouter/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: spouter version: 0.1.0 diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go deleted file mode 100644 index 6cb917915..000000000 --- a/pkg/downloader/chart_downloader.go +++ /dev/null @@ -1,369 +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 downloader - -import ( - "fmt" - "io" - "io/ioutil" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - - "helm.sh/helm/pkg/getter" - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/provenance" - "helm.sh/helm/pkg/repo" - "helm.sh/helm/pkg/urlutil" -) - -// VerificationStrategy describes a strategy for determining whether to verify a chart. -type VerificationStrategy int - -const ( - // VerifyNever will skip all verification of a chart. - VerifyNever VerificationStrategy = iota - // VerifyIfPossible will attempt a verification, it will not error if verification - // data is missing. But it will not stop processing if verification fails. - VerifyIfPossible - // VerifyAlways will always attempt a verification, and will fail if the - // verification fails. - VerifyAlways - // VerifyLater will fetch verification data, but not do any verification. - // This is to accommodate the case where another step of the process will - // perform verification. - VerifyLater -) - -// ErrNoOwnerRepo indicates that a given chart URL can't be found in any repos. -var ErrNoOwnerRepo = errors.New("could not find a repo containing the given URL") - -// ChartDownloader handles downloading a chart. -// -// It is capable of performing verifications on charts as well. -type ChartDownloader struct { - // Out is the location to write warning and info messages. - Out io.Writer - // Verify indicates what verification strategy to use. - Verify VerificationStrategy - // Keyring is the keyring file used for verification. - Keyring string - // HelmHome is the $HELM_HOME. - HelmHome helmpath.Home - // Getter collection for the operation - Getters getter.Providers - // Chart repository username - Username string - // Chart repository password - Password string -} - -// DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. -// -// If Verify is set to VerifyNever, the verification will be nil. -// If Verify is set to VerifyIfPossible, this will return a verification (or nil on failure), and print a warning on failure. -// If Verify is set to VerifyAlways, this will return a verification or an error if the verification fails. -// If Verify is set to VerifyLater, this will download the prov file (if it exists), but not verify it. -// -// For VerifyNever and VerifyIfPossible, the Verification may be empty. -// -// Returns a string path to the location where the file was downloaded and a verification -// (if provenance was verified), or an error if something bad happened. -func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { - u, r, g, err := c.ResolveChartVersionAndGetRepo(ref, version) - if err != nil { - return "", nil, err - } - - data, err := g.Get(u.String()) - if err != nil { - return "", nil, err - } - - name := filepath.Base(u.Path) - destfile := filepath.Join(dest, name) - if err := ioutil.WriteFile(destfile, data.Bytes(), 0644); err != nil { - return destfile, nil, err - } - - // If provenance is requested, verify it. - ver := &provenance.Verification{} - if c.Verify > VerifyNever { - body, err := r.Client.Get(u.String() + ".prov") - if err != nil { - if c.Verify == VerifyAlways { - return destfile, ver, errors.Errorf("failed to fetch provenance %q", u.String()+".prov") - } - fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) - return destfile, ver, nil - } - provfile := destfile + ".prov" - if err := ioutil.WriteFile(provfile, body.Bytes(), 0644); err != nil { - return destfile, nil, err - } - - if c.Verify != VerifyLater { - ver, err = VerifyChart(destfile, c.Keyring) - if err != nil { - // Fail always in this case, since it means the verification step - // failed. - return destfile, ver, err - } - } - } - return destfile, ver, nil -} - -// ResolveChartVersion resolves a chart reference to a URL. -// -// It returns the URL as well as a preconfigured repo.Getter that can fetch -// the URL. -// -// A reference may be an HTTP URL, a 'reponame/chartname' reference, or a local path. -// -// A version is a SemVer string (1.2.3-beta.1+f334a6789). -// -// - For fully qualified URLs, the version will be ignored (since URLs aren't versioned) -// - For a chart reference -// * If version is non-empty, this will return the URL for that version -// * If version is empty, this will return the URL for the latest version -// * If no version can be found, an error is returned -func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, getter.Getter, error) { - u, r, _, err := c.ResolveChartVersionAndGetRepo(ref, version) - if r != nil { - return u, r.Client, err - } - return u, nil, err -} - -// ResolveChartVersionAndGetRepo is the same as the ResolveChartVersion method, but returns the chart repositoryy. -func (c *ChartDownloader) ResolveChartVersionAndGetRepo(ref, version string) (*url.URL, *repo.ChartRepository, *getter.HTTPGetter, error) { - u, err := url.Parse(ref) - if err != nil { - return nil, nil, nil, errors.Errorf("invalid chart URL format: %s", ref) - } - - rf, err := repo.LoadFile(c.HelmHome.RepositoryFile()) - if err != nil { - return u, nil, nil, err - } - - // TODO add user-agent - g, err := getter.NewHTTPGetter(ref, "", "", "") - if err != nil { - return u, nil, nil, err - } - - if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { - // In this case, we have to find the parent repo that contains this chart - // URL. And this is an unfortunate problem, as it requires actually going - // through each repo cache file and finding a matching URL. But basically - // we want to find the repo in case we have special SSL cert config - // for that repo. - - rc, err := c.scanReposForURL(ref, rf) - if err != nil { - // If there is no special config, return the default HTTP client and - // swallow the error. - if err == ErrNoOwnerRepo { - r := &repo.ChartRepository{} - r.Client = g - g.SetCredentials(c.getRepoCredentials(r)) - return u, r, g, err - } - return u, nil, nil, err - } - r, err := repo.NewChartRepository(rc, c.Getters) - // If we get here, we don't need to go through the next phase of looking - // up the URL. We have it already. So we just return. - return u, r, g, err - } - - // See if it's of the form: repo/path_to_chart - p := strings.SplitN(u.Path, "/", 2) - if len(p) < 2 { - return u, nil, nil, errors.Errorf("non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) - } - - repoName := p[0] - chartName := p[1] - rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories) - - if err != nil { - return u, nil, nil, err - } - - r, err := repo.NewChartRepository(rc, c.Getters) - if err != nil { - return u, nil, nil, err - } - g.SetCredentials(c.getRepoCredentials(r)) - - // Next, we need to load the index, and actually look up the chart. - i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) - if err != nil { - return u, r, g, errors.Wrap(err, "no cached repo found. (try 'helm repo update')") - } - - cv, err := i.Get(chartName, version) - if err != nil { - return u, r, g, errors.Wrapf(err, "chart %q matching %s not found in %s index. (try 'helm repo update')", chartName, version, r.Config.Name) - } - - if len(cv.URLs) == 0 { - return u, r, g, errors.Errorf("chart %q has no downloadable URLs", ref) - } - - // TODO: Seems that picking first URL is not fully correct - u, err = url.Parse(cv.URLs[0]) - if err != nil { - return u, r, g, errors.Errorf("invalid chart URL format: %s", ref) - } - - // If the URL is relative (no scheme), prepend the chart repo's base URL - if !u.IsAbs() { - repoURL, err := url.Parse(rc.URL) - if err != nil { - return repoURL, r, nil, err - } - q := repoURL.Query() - // We need a trailing slash for ResolveReference to work, but make sure there isn't already one - repoURL.Path = strings.TrimSuffix(repoURL.Path, "/") + "/" - u = repoURL.ResolveReference(u) - u.RawQuery = q.Encode() - // TODO add user-agent - g, err := getter.NewHTTPGetter(rc.URL, "", "", "") - if err != nil { - return repoURL, r, nil, err - } - g.SetCredentials(c.getRepoCredentials(r)) - return u, r, g, err - } - - return u, r, g, nil -} - -// If this ChartDownloader is not configured to use credentials, and the chart repository sent as an argument is, -// then the repository's configured credentials are returned. -// Else, this ChartDownloader's credentials are returned. -func (c *ChartDownloader) getRepoCredentials(r *repo.ChartRepository) (username, password string) { - username = c.Username - password = c.Password - if r != nil && r.Config != nil { - if username == "" { - username = r.Config.Username - } - if password == "" { - password = r.Config.Password - } - } - return -} - -// VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. -// -// It assumes that a chart archive file is accompanied by a provenance file whose -// name is the archive file name plus the ".prov" extension. -func VerifyChart(path, keyring string) (*provenance.Verification, error) { - // For now, error out if it's not a tar file. - if fi, err := os.Stat(path); err != nil { - return nil, err - } else if fi.IsDir() { - return nil, errors.New("unpacked charts cannot be verified") - } else if !isTar(path) { - return nil, errors.New("chart must be a tgz file") - } - - provfile := path + ".prov" - if _, err := os.Stat(provfile); err != nil { - return nil, errors.Wrapf(err, "could not load provenance file %s", provfile) - } - - sig, err := provenance.NewFromKeyring(keyring, "") - if err != nil { - return nil, errors.Wrap(err, "failed to load keyring") - } - return sig.Verify(path, provfile) -} - -// isTar tests whether the given file is a tar file. -// -// Currently, this simply checks extension, since a subsequent function will -// untar the file and validate its binary format. -func isTar(filename string) bool { - return strings.ToLower(filepath.Ext(filename)) == ".tgz" -} - -func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Entry, error) { - for _, rc := range cfgs { - if rc.Name == name { - if rc.URL == "" { - return nil, errors.Errorf("no URL found for repository %s", name) - } - return rc, nil - } - } - return nil, errors.Errorf("repo %s not found", name) -} - -// scanReposForURL scans all repos to find which repo contains the given URL. -// -// This will attempt to find the given URL in all of the known repositories files. -// -// If the URL is found, this will return the repo entry that contained that URL. -// -// If all of the repos are checked, but the URL is not found, an ErrNoOwnerRepo -// error is returned. -// -// Other errors may be returned when repositories cannot be loaded or searched. -// -// Technically, the fact that a URL is not found in a repo is not a failure indication. -// Charts are not required to be included in an index before they are valid. So -// be mindful of this case. -// -// The same URL can technically exist in two or more repositories. This algorithm -// will return the first one it finds. Order is determined by the order of repositories -// in the repositories.yaml file. -func (c *ChartDownloader) scanReposForURL(u string, rf *repo.File) (*repo.Entry, error) { - // FIXME: This is far from optimal. Larger installations and index files will - // incur a performance hit for this type of scanning. - for _, rc := range rf.Repositories { - r, err := repo.NewChartRepository(rc, c.Getters) - if err != nil { - return nil, err - } - - i, err := repo.LoadIndexFile(c.HelmHome.CacheIndex(r.Config.Name)) - if err != nil { - return nil, errors.Wrap(err, "no cached repo found. (try 'helm repo update')") - } - - for _, entry := range i.Entries { - for _, ver := range entry { - for _, dl := range ver.URLs { - if urlutil.Equal(u, dl) { - return rc, nil - } - } - } - } - } - // This means that there is no repo file for the given URL. - return nil, ErrNoOwnerRepo -} diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go deleted file mode 100644 index 33964641b..000000000 --- a/pkg/downloader/chart_downloader_test.go +++ /dev/null @@ -1,315 +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 downloader - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "os" - "path/filepath" - "testing" - - "helm.sh/helm/pkg/cli" - "helm.sh/helm/pkg/getter" - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/repo" - "helm.sh/helm/pkg/repo/repotest" -) - -func TestResolveChartRef(t *testing.T) { - tests := []struct { - name, ref, expect, version string - fail bool - }{ - {name: "full URL", ref: "http://example.com/foo-1.2.3.tgz", expect: "http://example.com/foo-1.2.3.tgz"}, - {name: "full URL, HTTPS", ref: "https://example.com/foo-1.2.3.tgz", expect: "https://example.com/foo-1.2.3.tgz"}, - {name: "full URL, with authentication", ref: "http://username:password@example.com/foo-1.2.3.tgz", expect: "http://username:password@example.com/foo-1.2.3.tgz"}, - {name: "reference, testing repo", ref: "testing/alpine", expect: "http://example.com/alpine-1.2.3.tgz"}, - {name: "reference, version, testing repo", ref: "testing/alpine", version: "0.2.0", expect: "http://example.com/alpine-0.2.0.tgz"}, - {name: "reference, version, malformed repo", ref: "malformed/alpine", version: "1.2.3", expect: "http://dl.example.com/alpine-1.2.3.tgz"}, - {name: "reference, querystring repo", ref: "testing-querystring/alpine", expect: "http://example.com/alpine-1.2.3.tgz?key=value"}, - {name: "reference, testing-relative repo", ref: "testing-relative/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"}, - {name: "reference, testing-relative repo", ref: "testing-relative/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"}, - {name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"}, - {name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"}, - {name: "full URL, HTTPS, irrelevant version", ref: "https://example.com/foo-1.2.3.tgz", version: "0.1.0", expect: "https://example.com/foo-1.2.3.tgz", fail: true}, - {name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true}, - {name: "invalid", ref: "invalid-1.2.3", fail: true}, - {name: "not found", ref: "nosuchthing/invalid-1.2.3", fail: true}, - } - - c := ChartDownloader{ - HelmHome: helmpath.Home("testdata/helmhome"), - Out: os.Stderr, - Getters: getter.All(cli.EnvSettings{}), - } - - for _, tt := range tests { - u, _, err := c.ResolveChartVersion(tt.ref, tt.version) - if err != nil { - if tt.fail { - continue - } - t.Errorf("%s: failed with error %s", tt.name, err) - continue - } - if got := u.String(); got != tt.expect { - t.Errorf("%s: expected %s, got %s", tt.name, tt.expect, got) - } - } -} - -func TestVerifyChart(t *testing.T) { - v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/helm-test-key.pub") - if err != nil { - t.Fatal(err) - } - // The verification is tested at length in the provenance package. Here, - // we just want a quick sanity check that the v is not empty. - if len(v.FileHash) == 0 { - t.Error("Digest missing") - } -} - -func TestDownload(t *testing.T) { - expect := "Call me Ishmael" - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, expect) - })) - defer srv.Close() - - provider, err := getter.ByScheme("http", cli.EnvSettings{}) - if err != nil { - t.Fatal("No http provider found") - } - - g, err := provider.New(srv.URL, "", "", "") - if err != nil { - t.Fatal(err) - } - got, err := g.Get(srv.URL) - if err != nil { - t.Fatal(err) - } - - if got.String() != expect { - t.Errorf("Expected %q, got %q", expect, got.String()) - } - - // test with server backed by basic auth - basicAuthSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - if !ok || username != "username" || password != "password" { - t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) - } - fmt.Fprint(w, expect) - })) - - defer basicAuthSrv.Close() - - u, _ := url.ParseRequestURI(basicAuthSrv.URL) - httpgetter, err := getter.NewHTTPGetter(u.String(), "", "", "") - if err != nil { - t.Fatal(err) - } - httpgetter.SetCredentials("username", "password") - got, err = httpgetter.Get(u.String()) - if err != nil { - t.Fatal(err) - } - - if got.String() != expect { - t.Errorf("Expected %q, got %q", expect, got.String()) - } -} - -func TestIsTar(t *testing.T) { - tests := map[string]bool{ - "foo.tgz": true, - "foo/bar/baz.tgz": true, - "foo-1.2.3.4.5.tgz": true, - "foo.tar.gz": false, // for our purposes - "foo.tgz.1": false, - "footgz": false, - } - - for src, expect := range tests { - if isTar(src) != expect { - t.Errorf("%q should be %t", src, expect) - } - } -} - -func TestDownloadTo(t *testing.T) { - tmp, err := ioutil.TempDir("", "helm-downloadto-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) - - hh := helmpath.Home(tmp) - dest := filepath.Join(hh.String(), "dest") - configDirectories := []string{ - hh.String(), - hh.Repository(), - hh.Cache(), - dest, - } - for _, p := range configDirectories { - if fi, err := os.Stat(p); err != nil { - if err := os.MkdirAll(p, 0755); err != nil { - t.Fatalf("Could not create %s: %s", p, err) - } - } else if !fi.IsDir() { - t.Fatalf("%s must be a directory", p) - } - } - - // Set up a fake repo - srv := repotest.NewServer(tmp) - defer srv.Stop() - if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil { - t.Error(err) - return - } - if err := srv.LinkIndices(); err != nil { - t.Fatal(err) - } - - c := ChartDownloader{ - HelmHome: hh, - Out: os.Stderr, - Verify: VerifyAlways, - Keyring: "testdata/helm-test-key.pub", - Getters: getter.All(cli.EnvSettings{}), - } - cname := "/signtest-0.1.0.tgz" - where, v, err := c.DownloadTo(srv.URL()+cname, "", dest) - if err != nil { - t.Error(err) - return - } - - if expect := filepath.Join(dest, cname); where != expect { - t.Errorf("Expected download to %s, got %s", expect, where) - } - - if v.FileHash == "" { - t.Error("File hash was empty, but verification is required.") - } - - if _, err := os.Stat(filepath.Join(dest, cname)); err != nil { - t.Error(err) - return - } -} - -func TestDownloadTo_VerifyLater(t *testing.T) { - tmp, err := ioutil.TempDir("", "helm-downloadto-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmp) - - hh := helmpath.Home(tmp) - dest := filepath.Join(hh.String(), "dest") - configDirectories := []string{ - hh.String(), - hh.Repository(), - hh.Cache(), - dest, - } - for _, p := range configDirectories { - if fi, err := os.Stat(p); err != nil { - if err := os.MkdirAll(p, 0755); err != nil { - t.Fatalf("Could not create %s: %s", p, err) - } - } else if !fi.IsDir() { - t.Fatalf("%s must be a directory", p) - } - } - - // Set up a fake repo - srv := repotest.NewServer(tmp) - defer srv.Stop() - if _, err := srv.CopyCharts("testdata/*.tgz*"); err != nil { - t.Error(err) - return - } - if err := srv.LinkIndices(); err != nil { - t.Fatal(err) - } - - c := ChartDownloader{ - HelmHome: hh, - Out: os.Stderr, - Verify: VerifyLater, - Getters: getter.All(cli.EnvSettings{}), - } - cname := "/signtest-0.1.0.tgz" - where, _, err := c.DownloadTo(srv.URL()+cname, "", dest) - if err != nil { - t.Error(err) - return - } - - if expect := filepath.Join(dest, cname); where != expect { - t.Errorf("Expected download to %s, got %s", expect, where) - } - - if _, err := os.Stat(filepath.Join(dest, cname)); err != nil { - t.Error(err) - return - } - if _, err := os.Stat(filepath.Join(dest, cname+".prov")); err != nil { - t.Error(err) - return - } -} - -func TestScanReposForURL(t *testing.T) { - hh := helmpath.Home("testdata/helmhome") - c := ChartDownloader{ - HelmHome: hh, - Out: os.Stderr, - Verify: VerifyLater, - Getters: getter.All(cli.EnvSettings{}), - } - - u := "http://example.com/alpine-0.2.0.tgz" - rf, err := repo.LoadFile(c.HelmHome.RepositoryFile()) - if err != nil { - t.Fatal(err) - } - - entry, err := c.scanReposForURL(u, rf) - if err != nil { - t.Fatal(err) - } - - if entry.Name != "testing" { - t.Errorf("Unexpected repo %q for URL %q", entry.Name, u) - } - - // A lookup failure should produce an ErrNoOwnerRepo - u = "https://no.such.repo/foo/bar-1.23.4.tgz" - if _, err = c.scanReposForURL(u, rf); err != ErrNoOwnerRepo { - t.Fatalf("expected ErrNoOwnerRepo, got %v", err) - } -} diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 779857405..4a5243cb2 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -19,12 +19,9 @@ import ( "fmt" "io" "io/ioutil" - "net/url" "os" - "path" "path/filepath" "strings" - "sync" "github.com/Masterminds/semver" "github.com/ghodss/yaml" @@ -33,11 +30,8 @@ import ( "helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart/loader" "helm.sh/helm/pkg/chartutil" - "helm.sh/helm/pkg/getter" - "helm.sh/helm/pkg/helmpath" "helm.sh/helm/pkg/repo" "helm.sh/helm/pkg/resolver" - "helm.sh/helm/pkg/urlutil" ) // Manager handles the lifecycle of fetching, resolving, and storing dependencies. @@ -46,18 +40,10 @@ type Manager struct { Out io.Writer // ChartPath is the path to the unpacked base chart upon which this operates. ChartPath string - // HelmHome is the $HELM_HOME directory - HelmHome helmpath.Home - // Verification indicates whether the chart should be verified. - Verify VerificationStrategy // Debug is the global "--debug" flag Debug bool - // Keyring is the key ring file. - Keyring string - // SkipUpdate indicates that the repository should not be updated first. - SkipUpdate bool - // Getter collection for the operation - Getters []getter.Provider + // Client for the operation + Client *repo.Client } // Build rebuilds a local charts directory from a lockfile. @@ -83,18 +69,6 @@ func (m *Manager) Build() error { return errors.New("Chart.lock is out of sync with Chart.yaml") } - // Check that all of the repos we're dependent on actually exist. - if err := m.hasAllRepos(lock.Dependencies); err != nil { - return err - } - - if !m.SkipUpdate { - // For each repo in the file, update the cached copy of that repo - if err := m.UpdateRepositories(); err != nil { - return err - } - } - // Now we need to fetch every package here into charts/ if err := m.downloadAll(lock.Dependencies); err != nil { return err @@ -128,23 +102,9 @@ func (m *Manager) Update() error { return err } - // Check that all of the repos we're dependent on actually exist and - // the repo index names. - repoNames, err := m.getRepoNames(req) - if err != nil { - return err - } - - // For each repo in the file, update the cached copy of that repo - if !m.SkipUpdate { - if err := m.UpdateRepositories(); err != nil { - return err - } - } - // Now we need to find out which version of a chart best satisfies the // dependencies in the Chart.yaml - lock, err := m.resolve(req, repoNames, hash) + lock, err := m.resolve(req, hash) if err != nil { return err } @@ -176,9 +136,9 @@ func (m *Manager) loadChartDir() (*chart.Chart, error) { // resolve takes a list of dependencies and translates them into an exact version to download. // // This returns a lock file, which has all of the dependencies normalized to a specific version. -func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string, hash string) (*chart.Lock, error) { - res := resolver.New(m.ChartPath, m.HelmHome) - return res.Resolve(req, repoNames, hash) +func (m *Manager) resolve(req []*chart.Dependency, hash string) (*chart.Lock, error) { + res := resolver.New(m.ChartPath, m.Client) + return res.Resolve(req, hash) } // downloadAll takes a list of dependencies and downloads them into charts/ @@ -186,11 +146,6 @@ func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string, // It will delete versions of the chart that exist on disk and might cause // a conflict. func (m *Manager) downloadAll(deps []*chart.Dependency) error { - repos, err := m.loadChartRepositories() - if err != nil { - return err - } - destPath := filepath.Join(m.ChartPath, "charts") tmpPath := filepath.Join(m.ChartPath, "tmpcharts") @@ -214,11 +169,11 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps)) var saveError error for _, dep := range deps { - if strings.HasPrefix(dep.Repository, "file://") { + if strings.HasPrefix(dep.Name, "file://") { if m.Debug { - fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) + fmt.Fprintf(m.Out, "Archiving %s\n", dep.Name) } - ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version) + ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Version) if err != nil { saveError = err break @@ -227,28 +182,27 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error { continue } - fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository) + fmt.Fprintf(m.Out, "Downloading %s\n", dep.Name) - // Any failure to resolve/download a chart should fail: - // https://github.com/helm/helm/issues/1439 - churl, username, password, err := findChartURL(dep.Name, dep.Version, dep.Repository, repos) + ref, err := repo.ParseNameTag(dep.Name, dep.Version) if err != nil { - saveError = errors.Wrapf(err, "could not find %s", churl) + saveError = errors.Wrapf(err, "could not parse dependency %q", dep.Name) + break + } + + if err := m.Client.PullChart(ref); err != nil { + saveError = errors.Wrapf(err, "could not download %q", ref.String()) break } - dl := ChartDownloader{ - Out: m.Out, - Verify: m.Verify, - Keyring: m.Keyring, - HelmHome: m.HelmHome, - Getters: m.Getters, - Username: username, - Password: password, + ch, err := m.Client.LoadChart(ref) + if err != nil { + saveError = errors.Wrapf(err, "could not download %q", ref.String()) + break } - if _, _, err := dl.DownloadTo(churl, "", destPath); err != nil { - saveError = errors.Wrapf(err, "could not download %s", churl) + if _, err := chartutil.Save(ch, destPath); err != nil { + saveError = errors.Wrapf(err, "could not download %s", ref.String()) break } } @@ -318,210 +272,6 @@ func (m *Manager) safeDeleteDep(name, dir string) error { return nil } -// hasAllRepos ensures that all of the referenced deps are in the local repo cache. -func (m *Manager) hasAllRepos(deps []*chart.Dependency) error { - rf, err := repo.LoadFile(m.HelmHome.RepositoryFile()) - if err != nil { - return err - } - repos := rf.Repositories - - // Verify that all repositories referenced in the deps are actually known - // by Helm. - missing := []string{} -Loop: - for _, dd := range deps { - // If repo is from local path, continue - if strings.HasPrefix(dd.Repository, "file://") { - continue - } - - if dd.Repository == "" { - continue - } - for _, repo := range repos { - if urlutil.Equal(repo.URL, strings.TrimSuffix(dd.Repository, "/")) { - continue Loop - } - } - missing = append(missing, dd.Repository) - } - if len(missing) > 0 { - return errors.Errorf("no repository definition for %s. Please add the missing repos via 'helm repo add'", strings.Join(missing, ", ")) - } - return nil -} - -// getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file. -func (m *Manager) getRepoNames(deps []*chart.Dependency) (map[string]string, error) { - rf, err := repo.LoadFile(m.HelmHome.RepositoryFile()) - if err != nil { - return nil, err - } - repos := rf.Repositories - - reposMap := make(map[string]string) - - // Verify that all repositories referenced in the deps are actually known - // by Helm. - missing := []string{} - for _, dd := range deps { - // if dep chart is from local path, verify the path is valid - if strings.HasPrefix(dd.Repository, "file://") { - if _, err := resolver.GetLocalPath(dd.Repository, m.ChartPath); err != nil { - return nil, err - } - - if m.Debug { - fmt.Fprintf(m.Out, "Repository from local path: %s\n", dd.Repository) - } - reposMap[dd.Name] = dd.Repository - continue - } - - found := false - - for _, repo := range repos { - if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) || - (strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) { - found = true - dd.Repository = repo.URL - reposMap[dd.Name] = repo.Name - break - } else if urlutil.Equal(repo.URL, dd.Repository) { - found = true - reposMap[dd.Name] = repo.Name - break - } - } - if !found { - missing = append(missing, dd.Repository) - } - } - if len(missing) > 0 { - errorMessage := fmt.Sprintf("no repository definition for %s. Please add them via 'helm repo add'", strings.Join(missing, ", ")) - // It is common for people to try to enter "stable" as a repository instead of the actual URL. - // For this case, let's give them a suggestion. - containsNonURL := false - for _, repo := range missing { - if !strings.Contains(repo, "//") && !strings.HasPrefix(repo, "@") && !strings.HasPrefix(repo, "alias:") { - containsNonURL = true - } - } - if containsNonURL { - errorMessage += ` -Note that repositories must be URLs or aliases. For example, to refer to the stable -repository, use "https://kubernetes-charts.storage.googleapis.com/" or "@stable" instead of -"stable". Don't forget to add the repo, too ('helm repo add').` - } - return nil, errors.New(errorMessage) - } - return reposMap, nil -} - -// UpdateRepositories updates all of the local repos to the latest. -func (m *Manager) UpdateRepositories() error { - rf, err := repo.LoadFile(m.HelmHome.RepositoryFile()) - if err != nil { - return err - } - repos := rf.Repositories - if len(repos) > 0 { - // This prints warnings straight to out. - if err := m.parallelRepoUpdate(repos); err != nil { - return err - } - } - return nil -} - -func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { - out := m.Out - fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") - var wg sync.WaitGroup - for _, c := range repos { - r, err := repo.NewChartRepository(c, m.Getters) - if err != nil { - return err - } - wg.Add(1) - go func(r *repo.ChartRepository) { - if err := r.DownloadIndexFile(m.HelmHome.Cache()); err != nil { - fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err) - } else { - fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", r.Config.Name) - } - wg.Done() - }(r) - } - wg.Wait() - fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") - return nil -} - -// findChartURL searches the cache of repo data for a chart that has the name and the repoURL specified. -// -// 'name' is the name of the chart. Version is an exact semver, or an empty string. If empty, the -// newest version will be returned. -// -// repoURL is the repository to search -// -// If it finds a URL that is "relative", it will prepend the repoURL. -func findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, err error) { - for _, cr := range repos { - if urlutil.Equal(repoURL, cr.Config.URL) { - var entry repo.ChartVersions - entry, err = findEntryByName(name, cr) - if err != nil { - return - } - var ve *repo.ChartVersion - ve, err = findVersionedEntry(version, entry) - if err != nil { - return - } - url, err = normalizeURL(repoURL, ve.URLs[0]) - if err != nil { - return - } - username = cr.Config.Username - password = cr.Config.Password - return - } - } - err = errors.Errorf("chart %s not found in %s", name, repoURL) - return -} - -// findEntryByName finds an entry in the chart repository whose name matches the given name. -// -// It returns the ChartVersions for that entry. -func findEntryByName(name string, cr *repo.ChartRepository) (repo.ChartVersions, error) { - for ename, entry := range cr.IndexFile.Entries { - if ename == name { - return entry, nil - } - } - return nil, errors.New("entry not found") -} - -// findVersionedEntry takes a ChartVersions list and returns a single chart version that satisfies the version constraints. -// -// If version is empty, the first chart found is returned. -func findVersionedEntry(version string, vers repo.ChartVersions) (*repo.ChartVersion, error) { - for _, verEntry := range vers { - if len(verEntry.URLs) == 0 { - // Not a legit entry. - continue - } - - if version == "" || versionEquals(version, verEntry.Version) { - return verEntry, nil - } - } - return nil, errors.New("no matching version") -} - func versionEquals(v1, v2 string) bool { sv1, err := semver.NewVersion(v1) if err != nil { @@ -535,55 +285,6 @@ func versionEquals(v1, v2 string) bool { return sv1.Equal(sv2) } -func normalizeURL(baseURL, urlOrPath string) (string, error) { - u, err := url.Parse(urlOrPath) - if err != nil { - return urlOrPath, err - } - if u.IsAbs() { - return u.String(), nil - } - u2, err := url.Parse(baseURL) - if err != nil { - return urlOrPath, errors.Wrap(err, "base URL failed to parse") - } - - u2.Path = path.Join(u2.Path, urlOrPath) - return u2.String(), nil -} - -// loadChartRepositories reads the repositories.yaml, and then builds a map of -// ChartRepositories. -// -// The key is the local name (which is only present in the repositories.yaml). -func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, error) { - indices := map[string]*repo.ChartRepository{} - repoyaml := m.HelmHome.RepositoryFile() - - // Load repositories.yaml file - rf, err := repo.LoadFile(repoyaml) - if err != nil { - return indices, errors.Wrapf(err, "failed to load %s", repoyaml) - } - - for _, re := range rf.Repositories { - lname := re.Name - cacheindex := m.HelmHome.CacheIndex(lname) - index, err := repo.LoadIndexFile(cacheindex) - if err != nil { - return indices, err - } - - // TODO: use constructor - cr := &repo.ChartRepository{ - Config: re, - IndexFile: index, - } - indices[lname] = cr - } - return indices, nil -} - // writeLock writes a lockfile to disk func writeLock(chartpath string, lock *chart.Lock) error { data, err := yaml.Marshal(lock) @@ -595,14 +296,14 @@ func writeLock(chartpath string, lock *chart.Lock) error { } // archive a dep chart from local directory and save it into charts/ -func tarFromLocalDir(chartpath, name, repo, version string) (string, error) { +func tarFromLocalDir(chartpath, name, version string) (string, error) { destPath := filepath.Join(chartpath, "charts") - if !strings.HasPrefix(repo, "file://") { - return "", errors.Errorf("wrong format: chart %s repository %s", name, repo) + if !strings.HasPrefix(name, "file://") { + return "", errors.Errorf("wrong format: chart %s", name) } - origPath, err := resolver.GetLocalPath(repo, chartpath) + origPath, err := resolver.GetLocalPath(name, chartpath) if err != nil { return "", err } diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index 8f1c74cf6..e138a75ba 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -16,12 +16,7 @@ limitations under the License. package downloader import ( - "bytes" - "reflect" "testing" - - "helm.sh/helm/pkg/chart" - "helm.sh/helm/pkg/helmpath" ) func TestVersionEquals(t *testing.T) { @@ -42,129 +37,3 @@ func TestVersionEquals(t *testing.T) { } } } - -func TestNormalizeURL(t *testing.T) { - tests := []struct { - name, base, path, expect string - }{ - {name: "basic URL", base: "https://example.com", path: "http://helm.sh/foo", expect: "http://helm.sh/foo"}, - {name: "relative path", base: "https://helm.sh/charts", path: "foo", expect: "https://helm.sh/charts/foo"}, - } - - for _, tt := range tests { - got, err := normalizeURL(tt.base, tt.path) - if err != nil { - t.Errorf("%s: error %s", tt.name, err) - continue - } else if got != tt.expect { - t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got) - } - } -} - -func TestFindChartURL(t *testing.T) { - b := bytes.NewBuffer(nil) - m := &Manager{ - Out: b, - HelmHome: helmpath.Home("testdata/helmhome"), - } - repos, err := m.loadChartRepositories() - if err != nil { - t.Fatal(err) - } - - name := "alpine" - version := "0.1.0" - repoURL := "http://example.com/charts" - - churl, username, password, err := findChartURL(name, version, repoURL, repos) - if err != nil { - t.Fatal(err) - } - if churl != "https://kubernetes-charts.storage.googleapis.com/alpine-0.1.0.tgz" { - t.Errorf("Unexpected URL %q", churl) - } - if username != "" { - t.Errorf("Unexpected username %q", username) - } - if password != "" { - t.Errorf("Unexpected password %q", password) - } -} - -func TestGetRepoNames(t *testing.T) { - b := bytes.NewBuffer(nil) - m := &Manager{ - Out: b, - HelmHome: helmpath.Home("testdata/helmhome"), - } - tests := []struct { - name string - req []*chart.Dependency - expect map[string]string - err bool - }{ - { - name: "no repo definition failure", - req: []*chart.Dependency{ - {Name: "oedipus-rex", Repository: "http://example.com/test"}, - }, - err: true, - }, - { - name: "no repo definition failure -- stable repo", - req: []*chart.Dependency{ - {Name: "oedipus-rex", Repository: "stable"}, - }, - err: true, - }, - { - name: "no repo definition failure", - req: []*chart.Dependency{ - {Name: "oedipus-rex", Repository: "http://example.com"}, - }, - expect: map[string]string{"oedipus-rex": "testing"}, - }, - { - name: "repo from local path", - req: []*chart.Dependency{ - {Name: "local-dep", Repository: "file://./testdata/signtest"}, - }, - expect: map[string]string{"local-dep": "file://./testdata/signtest"}, - }, - { - name: "repo alias (alias:)", - req: []*chart.Dependency{ - {Name: "oedipus-rex", Repository: "alias:testing"}, - }, - expect: map[string]string{"oedipus-rex": "testing"}, - }, - { - name: "repo alias (@)", - req: []*chart.Dependency{ - {Name: "oedipus-rex", Repository: "@testing"}, - }, - expect: map[string]string{"oedipus-rex": "testing"}, - }, - } - - for _, tt := range tests { - l, err := m.getRepoNames(tt.req) - if err != nil { - if tt.err { - continue - } - t.Fatal(err) - } - - if tt.err { - t.Fatalf("Expected error in test %q", tt.name) - } - - // m1 and m2 are the maps we want to compare - eq := reflect.DeepEqual(l, tt.expect) - if !eq { - t.Errorf("%s: expected map %v, got %v", tt.name, l, tt.name) - } - } -} diff --git a/pkg/downloader/testdata/signtest-0.1.0.tgz b/pkg/downloader/testdata/signtest-0.1.0.tgz index 6de9d988d..c74e5b0ef 100644 Binary files a/pkg/downloader/testdata/signtest-0.1.0.tgz and b/pkg/downloader/testdata/signtest-0.1.0.tgz differ diff --git a/pkg/downloader/testdata/signtest-0.1.0.tgz.prov b/pkg/downloader/testdata/signtest-0.1.0.tgz.prov index 94235399a..d325bb266 100644 --- a/pkg/downloader/testdata/signtest-0.1.0.tgz.prov +++ b/pkg/downloader/testdata/signtest-0.1.0.tgz.prov @@ -1,20 +1,21 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 +apiVersion: v1 description: A Helm chart for Kubernetes name: signtest version: 0.1.0 ... files: - signtest-0.1.0.tgz: sha256:dee72947753628425b82814516bdaa37aef49f25e8820dd2a6e15a33a007823b + signtest-0.1.0.tgz: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55 -----BEGIN PGP SIGNATURE----- -wsBcBAEBCgAQBQJXomNHCRCEO7+YH8GHYgAALywIAG1Me852Fpn1GYu8Q1GCcw4g -l2k7vOFchdDwDhdSVbkh4YyvTaIO3iE2Jtk1rxw+RIJiUr0eLO/rnIJuxZS8WKki -DR1LI9J1VD4dxN3uDETtWDWq7ScoPsRY5mJvYZXC8whrWEt/H2kfqmoA9LloRPWp -flOE0iktA4UciZOblTj6nAk3iDyjh/4HYL4a6tT0LjjKI7OTw4YyHfjHad1ywVCz -9dMUc1rPgTnl+fnRiSPSrlZIWKOt1mcQ4fVrU3nwtRUwTId2k8FtygL0G6M+Y6t0 -S6yaU7qfk9uTxkdkUF7Bf1X3ukxfe+cNBC32vf4m8LY4NkcYfSqK2fGtQsnVr6s= -=NyOM +wsBcBAEBCgAQBQJcoosfCRCEO7+YH8GHYgAA220IALAs8T8NPgkcLvHu+5109cAN +BOCNPSZDNsqLZW/2Dc9cKoBG7Jen4Qad+i5l9351kqn3D9Gm6eRfAWcjfggRobV/ +9daZ19h0nl4O1muQNAkjvdgZt8MOP3+PB3I3/Tu2QCYjI579SLUmuXlcZR5BCFPR +PJy+e3QpV2PcdeU2KZLG4tjtlrq+3QC9ZHHEJLs+BVN9d46Dwo6CxJdHJrrrAkTw +M8MhA92vbiTTPRSCZI9x5qDAwJYhoq0oxLflpuL2tIlo3qVoCsaTSURwMESEHO32 +XwYG7BaVDMELWhAorBAGBGBwWFbJ1677qQ2gd9CN0COiVhekWlFRcnn60800r84= +=k9Y9 -----END PGP SIGNATURE----- \ No newline at end of file diff --git a/pkg/downloader/testdata/signtest/Chart.yaml b/pkg/downloader/testdata/signtest/Chart.yaml index 90964b44a..f1f73723a 100644 --- a/pkg/downloader/testdata/signtest/Chart.yaml +++ b/pkg/downloader/testdata/signtest/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes name: signtest version: 0.1.0 diff --git a/pkg/downloader/testdata/signtest/alpine/Chart.yaml b/pkg/downloader/testdata/signtest/alpine/Chart.yaml index e45d7326a..eec261220 100644 --- a/pkg/downloader/testdata/signtest/alpine/Chart.yaml +++ b/pkg/downloader/testdata/signtest/alpine/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: Deploy a basic Alpine Linux pod home: https://helm.sh/helm name: alpine diff --git a/pkg/helmpath/helmhome.go b/pkg/helmpath/helmhome.go index 7641225c7..7c594fa94 100644 --- a/pkg/helmpath/helmhome.go +++ b/pkg/helmpath/helmhome.go @@ -40,21 +40,11 @@ func (h Home) Path(elem ...string) string { return filepath.Join(p...) } -// Registry returns the path to the local registry cache. -func (h Home) Registry() string { - return h.Path("registry") -} - // Repository returns the path to the local repository. func (h Home) Repository() string { return h.Path("repository") } -// RepositoryFile returns the path to the repositories.yaml file. -func (h Home) RepositoryFile() string { - return h.Path("repository", "repositories.yaml") -} - // Cache returns the path to the local cache. func (h Home) Cache() string { return h.Path("repository", "cache") diff --git a/pkg/helmpath/helmhome_unix_test.go b/pkg/helmpath/helmhome_unix_test.go index ea4a824df..dbfb625e1 100644 --- a/pkg/helmpath/helmhome_unix_test.go +++ b/pkg/helmpath/helmhome_unix_test.go @@ -31,9 +31,7 @@ func TestHelmHome(t *testing.T) { } isEq(t, hh.String(), "/r/users/helmtest") - isEq(t, hh.Registry(), "/r/users/helmtest/registry") isEq(t, hh.Repository(), "/r/users/helmtest/repository") - isEq(t, hh.RepositoryFile(), "/r/users/helmtest/repository/repositories.yaml") isEq(t, hh.Cache(), "/r/users/helmtest/repository/cache") isEq(t, hh.CacheIndex("t"), "/r/users/helmtest/repository/cache/t-index.yaml") isEq(t, hh.Starters(), "/r/users/helmtest/starters") diff --git a/pkg/helmpath/helmhome_windows_test.go b/pkg/helmpath/helmhome_windows_test.go index 71bc8ac44..24a90d9f0 100644 --- a/pkg/helmpath/helmhome_windows_test.go +++ b/pkg/helmpath/helmhome_windows_test.go @@ -28,9 +28,7 @@ func TestHelmHome(t *testing.T) { } isEq(t, hh.String(), "r:\\users\\helmtest") - isEq(t, hh.Registry(), "r:\\users\\helmtest\\registry") isEq(t, hh.Repository(), "r:\\users\\helmtest\\repository") - isEq(t, hh.RepositoryFile(), "r:\\users\\helmtest\\repository\\repositories.yaml") isEq(t, hh.Cache(), "r:\\users\\helmtest\\repository\\cache") isEq(t, hh.CacheIndex("t"), "r:\\users\\helmtest\\repository\\cache\\t-index.yaml") isEq(t, hh.Starters(), "r:\\users\\helmtest\\starters") diff --git a/pkg/lint/rules/testdata/albatross/Chart.yaml b/pkg/lint/rules/testdata/albatross/Chart.yaml index c108fa5e5..21124acfc 100644 --- a/pkg/lint/rules/testdata/albatross/Chart.yaml +++ b/pkg/lint/rules/testdata/albatross/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: albatross description: testing chart version: 199.44.12345-Alpha.1+cafe009 diff --git a/pkg/lint/rules/testdata/badchartfile/Chart.yaml b/pkg/lint/rules/testdata/badchartfile/Chart.yaml index dbb4a1501..b64052eb9 100644 --- a/pkg/lint/rules/testdata/badchartfile/Chart.yaml +++ b/pkg/lint/rules/testdata/badchartfile/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: A Helm chart for Kubernetes version: 0.0.0 home: "" diff --git a/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml b/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml index bed845249..632919d03 100644 --- a/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml +++ b/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: badvaluesfile description: A Helm chart for Kubernetes version: 0.0.1 diff --git a/pkg/lint/rules/testdata/goodone/Chart.yaml b/pkg/lint/rules/testdata/goodone/Chart.yaml index de05463ca..cb7a4bf20 100644 --- a/pkg/lint/rules/testdata/goodone/Chart.yaml +++ b/pkg/lint/rules/testdata/goodone/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 name: goodone description: good testing chart version: 199.44.12345-Alpha.1+cafe009 diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 7b06f47c8..544340242 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -227,10 +227,9 @@ func SetupPluginEnv(settings helm_env.EnvSettings, "HELM_HOME": settings.Home.String(), // Set vars that convey common information. - "HELM_PATH_REPOSITORY": settings.Home.Repository(), - "HELM_PATH_REPOSITORY_FILE": settings.Home.RepositoryFile(), - "HELM_PATH_CACHE": settings.Home.Cache(), - "HELM_PATH_STARTER": settings.Home.Starters(), + "HELM_PATH_REPOSITORY": settings.Home.Repository(), + "HELM_PATH_CACHE": settings.Home.Cache(), + "HELM_PATH_STARTER": settings.Home.Starters(), } { os.Setenv(key, val) } diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index d74e23887..1f4d2d232 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -63,13 +63,14 @@ const ( ) // testMessageBlock represents the expected message block for the testdata/hashtest chart. -const testMessageBlock = `description: Test chart versioning +const testMessageBlock = `apiVersion: v1 +description: Test chart versioning name: hashtest version: 1.2.3 ... files: - hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 + hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 ` func TestMessageBlock(t *testing.T) { @@ -100,7 +101,7 @@ func TestParseMessageBlock(t *testing.T) { if hash, ok := sc.Files["hashtest-1.2.3.tgz"]; !ok { t.Errorf("hashtest file not found in Files") - } else if hash != "sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75" { + } else if hash != "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888" { t.Errorf("Unexpected hash: %q", hash) } } diff --git a/pkg/provenance/testdata/hashtest-1.2.3.tgz b/pkg/provenance/testdata/hashtest-1.2.3.tgz index 1e89b524f..7bbc533ca 100644 Binary files a/pkg/provenance/testdata/hashtest-1.2.3.tgz and b/pkg/provenance/testdata/hashtest-1.2.3.tgz differ diff --git a/pkg/provenance/testdata/hashtest-1.2.3.tgz.prov b/pkg/provenance/testdata/hashtest-1.2.3.tgz.prov new file mode 100755 index 000000000..3a788cd2e --- /dev/null +++ b/pkg/provenance/testdata/hashtest-1.2.3.tgz.prov @@ -0,0 +1,21 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +apiVersion: v1 +description: Test chart versioning +name: hashtest +version: 1.2.3 + +... +files: + hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 +-----BEGIN PGP SIGNATURE----- + +wsBcBAEBCgAQBQJcon2ICRCEO7+YH8GHYgAASEAIAHD4Rad+LF47qNydI+k7x3aC +/qkdsqxE9kCUHtTJkZObE/Zmj2w3Opq0gcQftz4aJ2G9raqPDvwOzxnTxOkGfUdK +qIye48gFHzr2a7HnMTWr+HLQc4Gg+9kysIwkW4TM8wYV10osysYjBrhcafrHzFSK +791dBHhXP/aOrJQbFRob0GRFQ4pXdaSww1+kVaZLiKSPkkMKt9uk9Po1ggJYSIDX +uzXNcr78jTWACqkAtwx8+CJ8yzcGeuXSVNABDgbmAgpY0YT+Bz/UOWq4Q7tyuWnS +x9BKrvcb+Gc/6S0oK0Ffp8K4iSWYp79uH1bZ2oBS1yajA0c5h5i7qI3N4cabREw= +=YgnR +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/pkg/provenance/testdata/hashtest.sha256 b/pkg/provenance/testdata/hashtest.sha256 index 829031f9d..05173edf8 100644 --- a/pkg/provenance/testdata/hashtest.sha256 +++ b/pkg/provenance/testdata/hashtest.sha256 @@ -1 +1 @@ -8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 hashtest-1.2.3.tgz +c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 hashtest-1.2.3.tgz diff --git a/pkg/provenance/testdata/hashtest/Chart.yaml b/pkg/provenance/testdata/hashtest/Chart.yaml index 342631ef8..6edf5f8b6 100644 --- a/pkg/provenance/testdata/hashtest/Chart.yaml +++ b/pkg/provenance/testdata/hashtest/Chart.yaml @@ -1,3 +1,4 @@ +apiVersion: v1 description: Test chart versioning name: hashtest version: 1.2.3 diff --git a/pkg/provenance/testdata/msgblock.yaml b/pkg/provenance/testdata/msgblock.yaml index 0fdbda8ce..c16293ffc 100644 --- a/pkg/provenance/testdata/msgblock.yaml +++ b/pkg/provenance/testdata/msgblock.yaml @@ -1,7 +1,8 @@ +apiVersion: v1 description: Test chart versioning name: hashtest version: 1.2.3 ... files: - hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 + hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 diff --git a/pkg/provenance/testdata/msgblock.yaml.asc b/pkg/provenance/testdata/msgblock.yaml.asc index 5a34d6c52..b4187b742 100644 --- a/pkg/provenance/testdata/msgblock.yaml.asc +++ b/pkg/provenance/testdata/msgblock.yaml.asc @@ -1,21 +1,22 @@ -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 +apiVersion: v1 description: Test chart versioning name: hashtest version: 1.2.3 ... files: - hashtest-1.2.3.tgz: sha256:8e90e879e2a04b1900570e1c198755e46e4706d70b0e79f5edabfac7900e4e75 + hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 -----BEGIN PGP SIGNATURE----- -Comment: GPGTools - https://gpgtools.org -iQEcBAEBCgAGBQJXlp8KAAoJEIQ7v5gfwYdiE7sIAJYDiza+asekeooSXLvQiK+G -PKnveqQpx49EZ6L7Y7UlW25SyH8EjXXHeJysDywCXF3w4luxN9n56ffU0KEW11IY -F+JSjmgIWLS6ti7ZAGEi6JInQ/30rOAIpTEBRBL2IueW3m63mezrGK6XkBlGqpor -C9WKeqLi+DWlMoBtsEy3Uk0XP6pn/qBFICYAbLQQU0sCCUT8CBA8f8aidxi7aw9t -i404yYF+Dvc6i4JlSG77SV0ZJBWllUvsWoCd9Jli0NAuaMqmE7mzcEt/dE+Fm2Ql -Bx3tr1WS4xTRiFQdcOttOl93H+OaHTh+Y0qqLTzzpCvqmttG0HfI6lMeCs7LeyA= -=vEK+ +iQFJBAEBCgAzFiEEXmFTibU8o38O5gvThDu/mB/Bh2IFAlyiiDcVHGhlbG0tdGVz +dGluZ0BoZWxtLnNoAAoJEIQ7v5gfwYdiILAH/2f3GMVh+ZY5a+szOBudcuivjTcz +0Im1MwWQZfB1po3Yu7smWZbf5tJCzvVpYtvRlfa0nguuIh763MwOh9Q7dBXOLAxm +VCxqHm3svnNenBNfOpIygaMTgMZKxI4RrsKBgwPOTmlNtKg2lVaCiJAI30TXE6bB +/DwEYX0wmTssrAcSpTzOOSC+zHnPKew+5A3SY3ms+gAtVAcLepmJjI7RS7RhQxDl +AG+rWYis5gpDrk3U9OG1EOxqbftOAMqUl/kwI9eu5cPouN85rWwMe5pvHAvuyr/y +caYdlXDHTZsXmBuvfiUX6gqXtrpPCyKTCP+RzNf3+bXJM8m3u3gbMjGvKjU= +=vHcU -----END PGP SIGNATURE----- diff --git a/pkg/registry/client.go b/pkg/registry/client.go deleted file mode 100644 index a2244f816..000000000 --- a/pkg/registry/client.go +++ /dev/null @@ -1,159 +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/pkg/registry" - -import ( - "context" - "fmt" - "io" - - orascontent "github.com/deislabs/oras/pkg/content" - "github.com/deislabs/oras/pkg/oras" - "github.com/gosuri/uitable" - - "helm.sh/helm/pkg/chart" -) - -type ( - // ClientOptions is used to construct a new client - ClientOptions struct { - Out io.Writer - Resolver Resolver - CacheRootDir string - } - - // Client works with OCI-compliant registries and local Helm chart cache - Client struct { - out io.Writer - resolver Resolver - cache *filesystemCache // TODO: something more robust - } -) - -// NewClient returns a new registry client with config -func NewClient(options *ClientOptions) *Client { - return &Client{ - out: options.Out, - resolver: options.Resolver, - cache: &filesystemCache{ - out: options.Out, - rootDir: options.CacheRootDir, - store: orascontent.NewMemoryStore(), - }, - } -} - -// PushChart uploads a chart to a registry -func (c *Client) PushChart(ref *Reference) error { - c.setDefaultTag(ref) - fmt.Fprintf(c.out, "The push refers to repository [%s]\n", ref.Repo) - layers, err := c.cache.LoadReference(ref) - if err != nil { - return err - } - err = oras.Push(context.Background(), c.resolver, ref.String(), c.cache.store, layers) - if err != nil { - return err - } - var totalSize int64 - for _, layer := range layers { - totalSize += layer.Size - } - fmt.Fprintf(c.out, - "%s: pushed to remote (%d layers, %s total)\n", ref.Tag, len(layers), byteCountBinary(totalSize)) - return nil -} - -// PullChart downloads a chart from a registry -func (c *Client) PullChart(ref *Reference) error { - c.setDefaultTag(ref) - fmt.Fprintf(c.out, "%s: Pulling from %s\n", ref.Tag, ref.Repo) - layers, err := oras.Pull(context.Background(), c.resolver, ref.String(), c.cache.store, KnownMediaTypes()...) - if err != nil { - return err - } - exists, err := c.cache.StoreReference(ref, layers) - if err != nil { - return err - } - if !exists { - fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s:%s\n", ref.Repo, ref.Tag) - } else { - fmt.Fprintf(c.out, "Status: Chart is up to date for %s:%s\n", ref.Repo, ref.Tag) - } - return nil -} - -// SaveChart stores a copy of chart in local cache -func (c *Client) SaveChart(ch *chart.Chart, ref *Reference) error { - c.setDefaultTag(ref) - layers, err := c.cache.ChartToLayers(ch) - if err != nil { - return err - } - _, err = c.cache.StoreReference(ref, layers) - if err != nil { - return err - } - fmt.Fprintf(c.out, "%s: saved\n", ref.Tag) - return nil -} - -// LoadChart retrieves a chart object by reference -func (c *Client) LoadChart(ref *Reference) (*chart.Chart, error) { - c.setDefaultTag(ref) - layers, err := c.cache.LoadReference(ref) - if err != nil { - return nil, err - } - ch, err := c.cache.LayersToChart(layers) - return ch, err -} - -// RemoveChart deletes a locally saved chart -func (c *Client) RemoveChart(ref *Reference) error { - c.setDefaultTag(ref) - err := c.cache.DeleteReference(ref) - if err != nil { - return err - } - fmt.Fprintf(c.out, "%s: removed\n", ref.Tag) - return err -} - -// PrintChartTable prints a list of locally stored charts -func (c *Client) PrintChartTable() error { - table := uitable.New() - table.MaxColWidth = 60 - table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED") - rows, err := c.cache.TableRows() - if err != nil { - return err - } - for _, row := range rows { - table.AddRow(row...) - } - fmt.Fprintln(c.out, table.String()) - return nil -} - -func (c *Client) setDefaultTag(ref *Reference) { - if ref.Tag == "" { - ref.Tag = HelmChartDefaultTag - fmt.Fprintf(c.out, "Using default tag: %s\n", HelmChartDefaultTag) - } -} diff --git a/pkg/registry/client_test.go b/pkg/registry/client_test.go deleted file mode 100644 index aa3770ac1..000000000 --- a/pkg/registry/client_test.go +++ /dev/null @@ -1,187 +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 ( - "bytes" - "context" - "fmt" - "io" - "net" - "os" - "testing" - "time" - - "github.com/containerd/containerd/remotes/docker" - "github.com/docker/distribution/configuration" - "github.com/docker/distribution/registry" - _ "github.com/docker/distribution/registry/storage/driver/inmemory" - "github.com/stretchr/testify/suite" - - "helm.sh/helm/pkg/chart" -) - -var ( - testCacheRootDir = "helm-registry-test" -) - -type RegistryClientTestSuite struct { - suite.Suite - Out io.Writer - DockerRegistryHost string - CacheRootDir string - RegistryClient *Client -} - -func (suite *RegistryClientTestSuite) SetupSuite() { - suite.CacheRootDir = testCacheRootDir - - // Init test client - var out bytes.Buffer - suite.Out = &out - suite.RegistryClient = NewClient(&ClientOptions{ - Out: suite.Out, - Resolver: Resolver{ - Resolver: docker.NewResolver(docker.ResolverOptions{}), - }, - CacheRootDir: suite.CacheRootDir, - }) - - // Registry config - config := &configuration.Configuration{} - port, err := getFreePort() - if err != nil { - suite.Nil(err, "no error finding free port for test registry") - } - suite.DockerRegistryHost = fmt.Sprintf("localhost:%d", port) - config.HTTP.Addr = fmt.Sprintf(":%d", port) - config.HTTP.DrainTimeout = time.Duration(10) * time.Second - config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} - dockerRegistry, err := registry.NewRegistry(context.Background(), config) - suite.Nil(err, "no error creating test registry") - - // Start Docker registry - go dockerRegistry.ListenAndServe() -} - -func (suite *RegistryClientTestSuite) TearDownSuite() { - os.RemoveAll(suite.CacheRootDir) -} - -func (suite *RegistryClientTestSuite) Test_0_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{ - Name: "testchart", - Version: "1.2.3", - } - err = suite.RegistryClient.SaveChart(ch, ref) - suite.Nil(err) -} - -func (suite *RegistryClientTestSuite) Test_1_LoadChart() { - - // non-existent ref - ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) - suite.Nil(err) - ch, 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_2_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_3_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_4_PrintChartTable() { - err := suite.RegistryClient.PrintChartTable() - suite.Nil(err) -} - -func (suite *RegistryClientTestSuite) Test_5_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 TestRegistryClientTestSuite(t *testing.T) { - suite.Run(t, new(RegistryClientTestSuite)) -} - -// borrowed from https://github.com/phayes/freeport -func getFreePort() (int, error) { - addr, err := net.ResolveTCPAddr("tcp", "localhost:0") - if err != nil { - return 0, err - } - - l, err := net.ListenTCP("tcp", addr) - if err != nil { - return 0, err - } - defer l.Close() - return l.Addr().(*net.TCPAddr).Port, nil -} diff --git a/pkg/registry/reference.go b/pkg/registry/reference.go deleted file mode 100644 index 7a136205f..000000000 --- a/pkg/registry/reference.go +++ /dev/null @@ -1,134 +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/pkg/registry" - -import ( - "errors" - "regexp" - "strings" - - "github.com/containerd/containerd/reference" -) - -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 - 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 { - *reference.Spec - Tag string - Repo string - } -) - -// ParseReference converts a string to a Reference -func ParseReference(s string) (*Reference, error) { - spec, err := reference.Parse(s) - if err != nil { - return nil, err - } - - // convert to our custom type and make necessary mods - ref := Reference{Spec: &spec} - ref.setExtraFields() - - // ensure the reference is valid - err = ref.validate() - if err != nil { - return nil, err - } - - return &ref, nil -} - -// setExtraFields adds the Repo and Tag fields to a Reference -func (ref *Reference) setExtraFields() { - ref.Tag = ref.Object - ref.Repo = ref.Locator - ref.fixNoTag() - ref.fixNoRepo() -} - -// fixNoTag is a fix for ref strings such as "mychart:1.0.0", which result in missing tag -func (ref *Reference) fixNoTag() { - if ref.Tag == "" { - parts := strings.Split(ref.Repo, ":") - numParts := len(parts) - if 0 < numParts { - lastIndex := numParts - 1 - lastPart := parts[lastIndex] - if !strings.Contains(lastPart, "/") { - ref.Repo = strings.Join(parts[:lastIndex], ":") - ref.Tag = lastPart - } - } - } -} - -// fixNoRepo is a fix for ref strings such as "mychart", which have the repo swapped with tag -func (ref *Reference) fixNoRepo() { - if ref.Repo == "" { - ref.Repo = ref.Tag - 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 - } - return nil -} - -// validateNumColon 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) -} diff --git a/pkg/registry/reference_test.go b/pkg/registry/reference_test.go deleted file mode 100644 index e9ec024bc..000000000 --- a/pkg/registry/reference_test.go +++ /dev/null @@ -1,89 +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 TestReference(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) - - s = "mychart:1.5.0" - ref, err = ParseReference(s) - is.NoError(err) - is.Equal("mychart", ref.Repo) - is.Equal("1.5.0", ref.Tag) - - s = "myrepo/mychart" - ref, err = ParseReference(s) - is.NoError(err) - is.Equal("myrepo/mychart", ref.Repo) - is.Equal("", ref.Tag) - - 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) - - 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) - - 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) - - s = "localhost:5000/mychart:latest" - ref, err = ParseReference(s) - is.NoError(err) - is.Equal("localhost:5000/mychart", ref.Repo) - is.Equal("latest", ref.Tag) - - 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) -} diff --git a/pkg/registry/cache.go b/pkg/repo/cache.go similarity index 93% rename from pkg/registry/cache.go rename to pkg/repo/cache.go index 96911c3db..c0cc3fd9e 100644 --- a/pkg/registry/cache.go +++ b/pkg/repo/cache.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry // import "helm.sh/helm/pkg/registry" +package repo // import "helm.sh/helm/pkg/repo" import ( "bytes" @@ -28,9 +28,10 @@ import ( "strings" "time" + "github.com/containerd/containerd/reference" orascontent "github.com/deislabs/oras/pkg/content" "github.com/docker/go-units" - checksum "github.com/opencontainers/go-digest" + digest "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -74,6 +75,7 @@ func (cache *filesystemCache) LayersToChart(layers []ocispec.Descriptor) (*chart if err != nil { return nil, err } + metadata.APIVersion = chart.APIVersionV1 metadata.Name = name metadata.Version = version @@ -96,8 +98,8 @@ func (cache *filesystemCache) LayersToChart(layers []ocispec.Descriptor) (*chart func (cache *filesystemCache) ChartToLayers(ch *chart.Chart) ([]ocispec.Descriptor, error) { // extract/separate the name and version from other metadata - if ch.Metadata == nil { - return nil, errors.New("chart does not contain metadata") + if err := ch.Validate(); err != nil { + return nil, err } name := ch.Metadata.Name version := ch.Metadata.Version @@ -115,7 +117,11 @@ func (cache *filesystemCache) ChartToLayers(ch *chart.Chart) ([]ocispec.Descript // TODO: something better than this hack. Currently needed for chartutil.Save() // If metadata does not contain Name or Version, an error is returned // such as "no chart name specified (Chart.yaml)" - ch.Metadata = &chart.Metadata{Name: "-", Version: "-"} + ch.Metadata = &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "-", + Version: "0.1.0", + } destDir := mkdir(filepath.Join(cache.rootDir, "blobs", ".build")) tmpFile, err := chartutil.Save(ch, destDir) defer os.Remove(tmpFile) @@ -136,8 +142,8 @@ func (cache *filesystemCache) ChartToLayers(ch *chart.Chart) ([]ocispec.Descript return layers, nil } -func (cache *filesystemCache) LoadReference(ref *Reference) ([]ocispec.Descriptor, error) { - tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tagOrDefault(ref.Tag)) +func (cache *filesystemCache) LoadReference(ref reference.Spec) ([]ocispec.Descriptor, error) { + tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Locator), "tags", tagOrDefault(ref.Object)) // add meta layer metaJSONRaw, err := getSymlinkDestContent(filepath.Join(tagDir, "meta")) @@ -164,9 +170,9 @@ func (cache *filesystemCache) LoadReference(ref *Reference) ([]ocispec.Descripto return layers, nil } -func (cache *filesystemCache) StoreReference(ref *Reference, layers []ocispec.Descriptor) (bool, error) { - tag := tagOrDefault(ref.Tag) - tagDir := mkdir(filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tag)) +func (cache *filesystemCache) StoreReference(ref reference.Spec, layers []ocispec.Descriptor) (bool, error) { + tag := tagOrDefault(ref.Object) + tagDir := mkdir(filepath.Join(cache.rootDir, "refs", escape(ref.Locator), "tags", tag)) // Retrieve just the meta and content layers metaLayer, contentLayer, err := extractLayers(layers) @@ -238,8 +244,8 @@ func (cache *filesystemCache) StoreReference(ref *Reference, layers []ocispec.De return metaExists && contentExists, nil } -func (cache *filesystemCache) DeleteReference(ref *Reference) error { - tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Repo), "tags", tagOrDefault(ref.Tag)) +func (cache *filesystemCache) DeleteReference(ref reference.Spec) error { + tagDir := filepath.Join(cache.rootDir, "refs", escape(ref.Locator), "tags", tagOrDefault(ref.Object)) if _, err := os.Stat(tagDir); os.IsNotExist(err) { return errors.New("ref not found") } @@ -370,7 +376,7 @@ func createChartFile(chartsRootDir string, name string, version string) (string, } // digestPath returns the path to addressable content, and whether the file exists -func digestPath(rootDir string, digest checksum.Digest) (bool, string) { +func digestPath(rootDir string, digest digest.Digest) (bool, string) { path := filepath.Join(rootDir, "sha256", digest.Hex()) exists := fileExists(path) return exists, path diff --git a/pkg/repo/chartrepo.go b/pkg/repo/chartrepo.go deleted file mode 100644 index 9dab8be94..000000000 --- a/pkg/repo/chartrepo.go +++ /dev/null @@ -1,272 +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 repo // import "helm.sh/helm/pkg/repo" - -import ( - "fmt" - "io/ioutil" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/ghodss/yaml" - "github.com/pkg/errors" - - "helm.sh/helm/pkg/chart/loader" - "helm.sh/helm/pkg/getter" - "helm.sh/helm/pkg/provenance" -) - -// Entry represents a collection of parameters for chart repository -type Entry struct { - Name string `json:"name"` - Cache string `json:"cache"` - URL string `json:"url"` - Username string `json:"username"` - Password string `json:"password"` - CertFile string `json:"certFile"` - KeyFile string `json:"keyFile"` - CAFile string `json:"caFile"` -} - -// ChartRepository represents a chart repository -type ChartRepository struct { - Config *Entry - ChartPaths []string - IndexFile *IndexFile - Client getter.Getter -} - -// NewChartRepository constructs ChartRepository -func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository, error) { - u, err := url.Parse(cfg.URL) - if err != nil { - return nil, errors.Errorf("invalid chart URL format: %s", cfg.URL) - } - - getterConstructor, err := getters.ByScheme(u.Scheme) - if err != nil { - return nil, errors.Errorf("could not find protocol handler for: %s", u.Scheme) - } - client, err := getterConstructor(cfg.URL, cfg.CertFile, cfg.KeyFile, cfg.CAFile) - if err != nil { - return nil, errors.Wrapf(err, "could not construct protocol handler for: %s", u.Scheme) - } - - return &ChartRepository{ - Config: cfg, - IndexFile: NewIndexFile(), - Client: client, - }, nil -} - -// Load loads a directory of charts as if it were a repository. -// -// It requires the presence of an index.yaml file in the directory. -func (r *ChartRepository) Load() error { - dirInfo, err := os.Stat(r.Config.Name) - if err != nil { - return err - } - if !dirInfo.IsDir() { - return errors.Errorf("%q is not a directory", r.Config.Name) - } - - // FIXME: Why are we recursively walking directories? - // FIXME: Why are we not reading the repositories.yaml to figure out - // what repos to use? - filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, err error) error { - if !f.IsDir() { - if strings.Contains(f.Name(), "-index.yaml") { - i, err := LoadIndexFile(path) - if err != nil { - return nil - } - r.IndexFile = i - } else if strings.HasSuffix(f.Name(), ".tgz") { - r.ChartPaths = append(r.ChartPaths, path) - } - } - return nil - }) - return nil -} - -// DownloadIndexFile fetches the index from a repository. -// -// cachePath is prepended to any index that does not have an absolute path. This -// is for pre-2.2.0 repo files. -func (r *ChartRepository) DownloadIndexFile(cachePath string) error { - var indexURL string - parsedURL, err := url.Parse(r.Config.URL) - if err != nil { - return err - } - parsedURL.Path = strings.TrimSuffix(parsedURL.Path, "/") + "/index.yaml" - - indexURL = parsedURL.String() - // TODO add user-agent - g, err := getter.NewHTTPGetter(indexURL, r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile) - if err != nil { - return err - } - g.SetCredentials(r.Config.Username, r.Config.Password) - resp, err := g.Get(indexURL) - if err != nil { - return err - } - - index, err := ioutil.ReadAll(resp) - if err != nil { - return err - } - - if _, err := loadIndex(index); err != nil { - return err - } - - // In Helm 2.2.0 the config.cache was accidentally switched to an absolute - // path, which broke backward compatibility. This fixes it by prepending a - // global cache path to relative paths. - // - // It is changed on DownloadIndexFile because that was the method that - // originally carried the cache path. - cp := r.Config.Cache - if !filepath.IsAbs(cp) { - cp = filepath.Join(cachePath, cp) - } - - return ioutil.WriteFile(cp, index, 0644) -} - -// Index generates an index for the chart repository and writes an index.yaml file. -func (r *ChartRepository) Index() error { - err := r.generateIndex() - if err != nil { - return err - } - return r.saveIndexFile() -} - -func (r *ChartRepository) saveIndexFile() error { - index, err := yaml.Marshal(r.IndexFile) - if err != nil { - return err - } - return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644) -} - -func (r *ChartRepository) generateIndex() error { - for _, path := range r.ChartPaths { - ch, err := loader.Load(path) - if err != nil { - return err - } - - digest, err := provenance.DigestFile(path) - if err != nil { - return err - } - - if !r.IndexFile.Has(ch.Name(), ch.Metadata.Version) { - r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest) - } - // TODO: If a chart exists, but has a different Digest, should we error? - } - r.IndexFile.SortEntries() - return nil -} - -// FindChartInRepoURL finds chart in chart repository pointed by repoURL -// without adding repo to repositories -func FindChartInRepoURL(repoURL, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) { - return FindChartInAuthRepoURL(repoURL, "", "", chartName, chartVersion, certFile, keyFile, caFile, getters) -} - -// FindChartInAuthRepoURL finds chart in chart repository pointed by repoURL -// without adding repo to repositories, like FindChartInRepoURL, -// but it also receives credentials for the chart repository. -func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) { - - // Download and write the index file to a temporary location - tempIndexFile, err := ioutil.TempFile("", "tmp-repo-file") - if err != nil { - return "", errors.Errorf("cannot write index file for repository requested") - } - defer os.Remove(tempIndexFile.Name()) - - c := Entry{ - URL: repoURL, - Username: username, - Password: password, - CertFile: certFile, - KeyFile: keyFile, - CAFile: caFile, - } - r, err := NewChartRepository(&c, getters) - if err != nil { - return "", err - } - if err := r.DownloadIndexFile(tempIndexFile.Name()); err != nil { - return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURL) - } - - // Read the index file for the repository to get chart information and return chart URL - repoIndex, err := LoadIndexFile(tempIndexFile.Name()) - if err != nil { - return "", err - } - - errMsg := fmt.Sprintf("chart %q", chartName) - if chartVersion != "" { - errMsg = fmt.Sprintf("%s version %q", errMsg, chartVersion) - } - cv, err := repoIndex.Get(chartName, chartVersion) - if err != nil { - return "", errors.Errorf("%s not found in %s repository", errMsg, repoURL) - } - - if len(cv.URLs) == 0 { - return "", errors.Errorf("%s has no downloadable URLs", errMsg) - } - - chartURL := cv.URLs[0] - - absoluteChartURL, err := ResolveReferenceURL(repoURL, chartURL) - if err != nil { - return "", errors.Wrap(err, "failed to make chart URL absolute") - } - - return absoluteChartURL, nil -} - -// ResolveReferenceURL resolves refURL relative to baseURL. -// If refURL is absolute, it simply returns refURL. -func ResolveReferenceURL(baseURL, refURL string) (string, error) { - parsedBaseURL, err := url.Parse(baseURL) - if err != nil { - return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL) - } - - parsedRefURL, err := url.Parse(refURL) - if err != nil { - return "", errors.Wrapf(err, "failed to parse %s as URL", refURL) - } - - return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil -} diff --git a/pkg/repo/chartrepo_test.go b/pkg/repo/chartrepo_test.go deleted file mode 100644 index 8e2071b01..000000000 --- a/pkg/repo/chartrepo_test.go +++ /dev/null @@ -1,297 +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 repo - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "reflect" - "strings" - "testing" - "time" - - "helm.sh/helm/pkg/chart" - "helm.sh/helm/pkg/cli" - "helm.sh/helm/pkg/getter" -) - -const ( - testRepository = "testdata/repository" - testURL = "http://example-charts.com" -) - -func TestLoadChartRepository(t *testing.T) { - r, err := NewChartRepository(&Entry{ - Name: testRepository, - URL: testURL, - }, getter.All(cli.EnvSettings{})) - if err != nil { - t.Errorf("Problem creating chart repository from %s: %v", testRepository, err) - } - - if err := r.Load(); err != nil { - t.Errorf("Problem loading chart repository from %s: %v", testRepository, err) - } - - paths := []string{ - filepath.Join(testRepository, "frobnitz-1.2.3.tgz"), - filepath.Join(testRepository, "sprocket-1.1.0.tgz"), - filepath.Join(testRepository, "sprocket-1.2.0.tgz"), - filepath.Join(testRepository, "universe/zarthal-1.0.0.tgz"), - } - - if r.Config.Name != testRepository { - t.Errorf("Expected %s as Name but got %s", testRepository, r.Config.Name) - } - - if !reflect.DeepEqual(r.ChartPaths, paths) { - t.Errorf("Expected %#v but got %#v\n", paths, r.ChartPaths) - } - - if r.Config.URL != testURL { - t.Errorf("Expected url for chart repository to be %s but got %s", testURL, r.Config.URL) - } -} - -func TestIndex(t *testing.T) { - r, err := NewChartRepository(&Entry{ - Name: testRepository, - URL: testURL, - }, getter.All(cli.EnvSettings{})) - if err != nil { - t.Errorf("Problem creating chart repository from %s: %v", testRepository, err) - } - - if err := r.Load(); err != nil { - t.Errorf("Problem loading chart repository from %s: %v", testRepository, err) - } - - err = r.Index() - if err != nil { - t.Errorf("Error performing index: %v\n", err) - } - - tempIndexPath := filepath.Join(testRepository, indexPath) - actual, err := LoadIndexFile(tempIndexPath) - defer os.Remove(tempIndexPath) // clean up - if err != nil { - t.Errorf("Error loading index file %v", err) - } - verifyIndex(t, actual) - - // Re-index and test again. - err = r.Index() - if err != nil { - t.Errorf("Error performing re-index: %s\n", err) - } - second, err := LoadIndexFile(tempIndexPath) - if err != nil { - t.Errorf("Error re-loading index file %v", err) - } - verifyIndex(t, second) -} - -func verifyIndex(t *testing.T, actual *IndexFile) { - var empty time.Time - if actual.Generated == empty { - t.Errorf("Generated should be greater than 0: %s", actual.Generated) - } - - if actual.APIVersion != APIVersionV1 { - t.Error("Expected v1 API") - } - - entries := actual.Entries - if numEntries := len(entries); numEntries != 3 { - t.Errorf("Expected 3 charts to be listed in index file but got %v", numEntries) - } - - expects := map[string]ChartVersions{ - "frobnitz": { - { - Metadata: &chart.Metadata{ - Name: "frobnitz", - Version: "1.2.3", - }, - }, - }, - "sprocket": { - { - Metadata: &chart.Metadata{ - Name: "sprocket", - Version: "1.2.0", - }, - }, - { - Metadata: &chart.Metadata{ - Name: "sprocket", - Version: "1.1.0", - }, - }, - }, - "zarthal": { - { - Metadata: &chart.Metadata{ - Name: "zarthal", - Version: "1.0.0", - }, - }, - }, - } - - for name, versions := range expects { - got, ok := entries[name] - if !ok { - t.Errorf("Could not find %q entry", name) - continue - } - if len(versions) != len(got) { - t.Errorf("Expected %d versions, got %d", len(versions), len(got)) - continue - } - for i, e := range versions { - g := got[i] - if e.Name != g.Name { - t.Errorf("Expected %q, got %q", e.Name, g.Name) - } - if e.Version != g.Version { - t.Errorf("Expected %q, got %q", e.Version, g.Version) - } - if len(g.Keywords) != 3 { - t.Error("Expected 3 keyrwords.") - } - if len(g.Maintainers) != 2 { - t.Error("Expected 2 maintainers.") - } - if g.Created == empty { - t.Error("Expected created to be non-empty") - } - if g.Description == "" { - t.Error("Expected description to be non-empty") - } - if g.Home == "" { - t.Error("Expected home to be non-empty") - } - if g.Digest == "" { - t.Error("Expected digest to be non-empty") - } - if len(g.URLs) != 1 { - t.Error("Expected exactly 1 URL") - } - } - } -} - -// startLocalServerForTests Start the local helm server -func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) { - if handler == nil { - fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml") - if err != nil { - return nil, err - } - handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(fileBytes) - }) - } - - return httptest.NewServer(handler), nil -} - -func TestFindChartInRepoURL(t *testing.T) { - srv, err := startLocalServerForTests(nil) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - - chartURL, err := FindChartInRepoURL(srv.URL, "nginx", "", "", "", "", getter.All(cli.EnvSettings{})) - if err != nil { - t.Errorf("%s", err) - } - if chartURL != "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz" { - t.Errorf("%s is not the valid URL", chartURL) - } - - chartURL, err = FindChartInRepoURL(srv.URL, "nginx", "0.1.0", "", "", "", getter.All(cli.EnvSettings{})) - if err != nil { - t.Errorf("%s", err) - } - if chartURL != "https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz" { - t.Errorf("%s is not the valid URL", chartURL) - } -} - -func TestErrorFindChartInRepoURL(t *testing.T) { - _, err := FindChartInRepoURL("http://someserver/something", "nginx", "", "", "", "", getter.All(cli.EnvSettings{})) - if err == nil { - t.Errorf("Expected error for bad chart URL, but did not get any errors") - } - if err != nil && !strings.Contains(err.Error(), `looks like "http://someserver/something" is not a valid chart repository or cannot be reached: Get http://someserver/something/index.yaml`) { - t.Errorf("Expected error for bad chart URL, but got a different error (%v)", err) - } - - srv, err := startLocalServerForTests(nil) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - - _, err = FindChartInRepoURL(srv.URL, "nginx1", "", "", "", "", getter.All(cli.EnvSettings{})) - if err == nil { - t.Errorf("Expected error for chart not found, but did not get any errors") - } - if err != nil && err.Error() != `chart "nginx1" not found in `+srv.URL+` repository` { - t.Errorf("Expected error for chart not found, but got a different error (%v)", err) - } - - _, err = FindChartInRepoURL(srv.URL, "nginx1", "0.1.0", "", "", "", getter.All(cli.EnvSettings{})) - if err == nil { - t.Errorf("Expected error for chart not found, but did not get any errors") - } - if err != nil && err.Error() != `chart "nginx1" version "0.1.0" not found in `+srv.URL+` repository` { - t.Errorf("Expected error for chart not found, but got a different error (%v)", err) - } - - _, err = FindChartInRepoURL(srv.URL, "chartWithNoURL", "", "", "", "", getter.All(cli.EnvSettings{})) - if err == nil { - t.Errorf("Expected error for no chart URLs available, but did not get any errors") - } - if err != nil && err.Error() != `chart "chartWithNoURL" has no downloadable URLs` { - t.Errorf("Expected error for chart not found, but got a different error (%v)", err) - } -} - -func TestResolveReferenceURL(t *testing.T) { - chartURL, err := ResolveReferenceURL("http://localhost:8123/charts/", "nginx-0.2.0.tgz") - if err != nil { - t.Errorf("%s", err) - } - if chartURL != "http://localhost:8123/charts/nginx-0.2.0.tgz" { - t.Errorf("%s", chartURL) - } - - chartURL, err = ResolveReferenceURL("http://localhost:8123", "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz") - if err != nil { - t.Errorf("%s", err) - } - if chartURL != "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz" { - t.Errorf("%s", chartURL) - } -} diff --git a/pkg/repo/client.go b/pkg/repo/client.go new file mode 100644 index 000000000..f0a0bcd10 --- /dev/null +++ b/pkg/repo/client.go @@ -0,0 +1,306 @@ +/* +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 repo // import "helm.sh/helm/pkg/repo" + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "path" + "sort" + + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/reference" + "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/remotes/docker" + orascontent "github.com/deislabs/oras/pkg/content" + "github.com/deislabs/oras/pkg/oras" + "github.com/gosuri/uitable" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + + "helm.sh/helm/pkg/chart" +) + +// VerificationStrategy describes a strategy for determining whether to verify a chart. +type VerificationStrategy int + +const ( + // VerifyNever will skip all verification of a chart. + VerifyNever VerificationStrategy = 1 << iota + // VerifyIfPossible will attempt a verification, it will not error if verification + // data is missing. But it will not stop processing if verification fails. + VerifyIfPossible + // VerifyAlways will always attempt a verification, and will fail if the + // verification fails. + VerifyAlways + // VerifyLater will fetch verification data, but not do any verification. + // This is to accommodate the case where another step of the process will + // perform verification. + VerifyLater +) + +// ClientOptions is used to construct a new client +type ClientOptions struct { + Out io.Writer + CacheRootDir string + // VerificationStrategy indicates what verification strategy to use. + // + // NOTE(bacongobbler): this is a no-op for now + VerificationStrategy VerificationStrategy +} + +// Client works with OCI-compliant registries and local Helm chart cache +type Client struct { + out io.Writer + resolver *Resolver + verificationStrategy VerificationStrategy + cache *filesystemCache // TODO: something more robust +} + +// NewClient returns a new registry client with config +func NewClient(options *ClientOptions) *Client { + return &Client{ + out: options.Out, + resolver: newResolver(docker.ResolverOptions{}), + verificationStrategy: options.VerificationStrategy, + cache: &filesystemCache{ + out: options.Out, + rootDir: options.CacheRootDir, + store: orascontent.NewMemoryStore(), + }, + } +} + +// PushChart uploads a chart to a registry +func (c *Client) PushChart(ref reference.Spec) error { + ref, err := c.setDefaultTag(ref) + if err != nil { + return err + } + fmt.Fprintf(c.out, "The push refers to repository [%s]\n", ref.Locator) + layers, err := c.cache.LoadReference(ref) + if err != nil { + return err + } + err = oras.Push(context.Background(), c.resolver, ref.String(), c.cache.store, layers) + if err != nil { + return err + } + var totalSize int64 + for _, layer := range layers { + totalSize += layer.Size + } + fmt.Fprintf(c.out, + "%s: pushed to remote (%d layers, %s total)\n", ref.Object, len(layers), byteCountBinary(totalSize)) + return nil +} + +// PullChart downloads a chart from a registry +func (c *Client) PullChart(ref reference.Spec) error { + ref, err := c.setDefaultTag(ref) + if err != nil { + return err + } + fmt.Fprintf(c.out, "%s: Pulling %s\n", ref.Locator, ref.Object) + layers, err := oras.Pull(context.Background(), c.resolver, ref.String(), c.cache.store, KnownMediaTypes()...) + if err != nil { + if err == reference.ErrObjectRequired { + return fmt.Errorf(`chart "%s" not found in repository`, ref.String()) + } + return err + } + + exists, err := c.cache.StoreReference(ref, layers) + if err != nil { + return err + } + if !exists { + fmt.Fprintf(c.out, "Status: Downloaded newer chart for %s:%s\n", ref.Locator, ref.Object) + } else { + fmt.Fprintf(c.out, "Status: Chart is up to date for %s:%s\n", ref.Locator, ref.Object) + } + + // TODO(bacongobbler): do we store and fetch the provenance file from one of the layers + // and verify it here, or do we sign the manifest using Notary and let oras handle + // the verification step? + // + // https://github.com/containerd/containerd/issues/395#issuecomment-268105862 + + return nil +} + +// SaveChart stores a copy of chart in local cache +func (c *Client) SaveChart(ch *chart.Chart, registry string) error { + // extract/separate the name and version from other metadata + if ch.Metadata == nil { + return errors.New("chart does not contain metadata") + } + name := ch.Metadata.Name + version := ch.Metadata.Version + + ref, err := reference.Parse(path.Join(registry, fmt.Sprintf("%s:%s", name, version))) + if err != nil { + return err + } + + layers, err := c.cache.ChartToLayers(ch) + if err != nil { + return err + } + _, err = c.cache.StoreReference(ref, layers) + if err != nil { + return err + } + fmt.Fprintf(c.out, "%s: saved\n", ref.String()) + return nil +} + +// LoadChart retrieves a chart object by reference +func (c *Client) LoadChart(ref reference.Spec) (*chart.Chart, error) { + ref, err := c.setDefaultTag(ref) + if err != nil { + return nil, err + } + layers, err := c.cache.LoadReference(ref) + if err != nil { + return nil, err + } + ch, err := c.cache.LayersToChart(layers) + return ch, err +} + +// RemoveChart deletes a locally saved chart +func (c *Client) RemoveChart(ref reference.Spec) error { + ref, err := c.setDefaultTag(ref) + if err != nil { + return err + } + err = c.cache.DeleteReference(ref) + if err != nil { + return err + } + fmt.Fprintf(c.out, "%s: removed\n", ref.Object) + return err +} + +// PrintChartTable prints a list of locally stored charts +func (c *Client) PrintChartTable() error { + table := uitable.New() + table.MaxColWidth = 60 + table.AddRow("REF", "NAME", "VERSION", "DIGEST", "SIZE", "CREATED") + rows, err := c.cache.TableRows() + if err != nil { + return err + } + for _, row := range rows { + table.AddRow(row...) + } + fmt.Fprintln(c.out, table.String()) + return nil +} + +func (c *Client) setDefaultTag(ref reference.Spec) (reference.Spec, error) { + + // skip if the tag is already set + if ref.Object != "" { + return ref, nil + } + + tags, err := c.FetchTags(ref.String()) + if err != nil { + return reference.Spec{}, err + } + if len(tags) == 0 { + return reference.Spec{}, fmt.Errorf("no tags were found for %s", ref.Locator) + } + + // we need to create a new reference as the digest has changed. + newRef, err := reference.Parse(fmt.Sprintf("%s:%s", ref.String(), tags[len(tags)-1])) + if err != nil { + return reference.Spec{}, err + } + + return newRef, err +} + +// FindChart finds a chart in the given chart repository. +func (c *Client) FindChart(ref reference.Spec) error { + r, err := c.setDefaultTag(ref) + if err != nil { + return fmt.Errorf(`chart "%s" not found in repository`, ref.String()) + } + return c.PullChart(r) +} + +// FetchTags fetches the tags for a particular repository. +func (c *Client) FetchTags(repo string) ([]string, error) { + ctx := context.Background() + + fetcher, err := c.tagFetcher(ctx, repo) + if err != nil { + return nil, fmt.Errorf("failed to get fetcher for %q: %v", repo, err) + } + + buf := bytes.NewBuffer(nil) + handlers := images.Handlers( + fetchHandler(buf, fetcher), + ) + + if err := images.Dispatch(ctx, handlers, ocispec.Descriptor{}); err != nil { + return nil, err + } + + var resp tagsResponse + err = json.Unmarshal(buf.Bytes(), &resp) + if err != nil { + return nil, err + } + sort.Sort(sort.Reverse(sort.StringSlice(resp.Tags))) + + return resp.Tags, nil +} + +func (c *Client) tagFetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { + refspec, err := reference.Parse(ref) + if err != nil { + return nil, err + } + + base, err := c.resolver.base(refspec) + if err != nil { + return nil, err + } + + return tagFetcher{ + baseResolver: base, + }, nil +} + +func fetchHandler(w io.Writer, fetcher remotes.Fetcher) images.HandlerFunc { + return func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) { + rc, err := fetcher.Fetch(ctx, desc) + if err != nil { + return nil, err + } + defer rc.Close() + _, err = io.Copy(w, rc) + return nil, err + } +} diff --git a/pkg/repo/client_test.go b/pkg/repo/client_test.go new file mode 100644 index 000000000..6fd175bd6 --- /dev/null +++ b/pkg/repo/client_test.go @@ -0,0 +1,204 @@ +/* +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 repo + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "testing" + + "github.com/containerd/containerd/reference" + _ "github.com/docker/distribution/registry/storage/driver/inmemory" + "github.com/stretchr/testify/suite" + + "helm.sh/helm/pkg/chart" + "helm.sh/helm/pkg/repo/repotest" +) + +type RegistryClientTestSuite struct { + suite.Suite + DockerRegistryHost string + RegistryClient *Client + CacheDir string +} + +func (suite *RegistryClientTestSuite) SetupSuite() { + var err error + suite.CacheDir, err = ioutil.TempDir("", "helm-registry-test") + suite.Nil(err) + + // Init test client + suite.RegistryClient = NewClient(&ClientOptions{ + Out: ioutil.Discard, + CacheRootDir: suite.CacheDir, + }) + + testRepo := repotest.NewServer() + suite.DockerRegistryHost = testRepo.URL() +} + +func (suite *RegistryClientTestSuite) TearDownSuite() { + suite.Nil(os.RemoveAll(suite.CacheDir)) +} + +func (suite *RegistryClientTestSuite) Test_0_SaveChart() { + // empty chart + suite.NotNil(suite.RegistryClient.SaveChart(&chart.Chart{}, suite.DockerRegistryHost)) + + // valid chart + ch := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "testchart", + Version: "1.2.3", + }, + } + + suite.Nil(suite.RegistryClient.SaveChart(ch, suite.DockerRegistryHost)) + + ch2 := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "testchart", + Version: "4.5.6", + }, + } + + suite.Nil(suite.RegistryClient.SaveChart(ch2, suite.DockerRegistryHost)) +} + +func (suite *RegistryClientTestSuite) Test_1_LoadChart() { + + // non-existent ref + ref, err := reference.Parse(path.Join(suite.DockerRegistryHost, "whodis:9.9.9")) + suite.Nil(err) + ch, err := suite.RegistryClient.LoadChart(ref) + suite.NotNil(err) + + // existing ref + ref, err = reference.Parse(path.Join(suite.DockerRegistryHost, "testchart:1.2.3")) + 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) + + // existing ref + ref2, err := reference.Parse(path.Join(suite.DockerRegistryHost, "testchart:4.5.6")) + suite.Nil(err) + ch, err = suite.RegistryClient.LoadChart(ref2) + suite.Nil(err) + suite.Equal("testchart", ch.Metadata.Name) + suite.Equal("4.5.6", ch.Metadata.Version) +} + +func (suite *RegistryClientTestSuite) Test_2_PushChart() { + + // non-existent ref + ref, err := reference.Parse(fmt.Sprintf("%s/whodis:9.9.9", suite.DockerRegistryHost)) + suite.Nil(err) + suite.NotNil(suite.RegistryClient.PushChart(ref)) + + // existing ref + ref, err = reference.Parse(path.Join(suite.DockerRegistryHost, "testchart:1.2.3")) + suite.Nil(err) + suite.Nil(suite.RegistryClient.PushChart(ref)) + + ref2, err := reference.Parse(path.Join(suite.DockerRegistryHost, "testchart:4.5.6")) + suite.Nil(err) + suite.Nil(suite.RegistryClient.PushChart(ref2)) +} + +func (suite *RegistryClientTestSuite) Test_3_PullChart() { + + // non-existent ref + ref, err := reference.Parse(fmt.Sprintf("%s/whodis:9.9.9", suite.DockerRegistryHost)) + suite.Nil(err) + suite.NotNil(suite.RegistryClient.PullChart(ref)) + + // existing ref + ref, err = reference.Parse(path.Join(suite.DockerRegistryHost, "testchart:1.2.3")) + suite.Nil(err) + suite.Nil(suite.RegistryClient.PullChart(ref)) +} + +func (suite *RegistryClientTestSuite) Test_4_PrintChartTable() { + err := suite.RegistryClient.PrintChartTable() + suite.Nil(err) +} + +func (suite *RegistryClientTestSuite) Test_5_RemoveChart() { + + // non-existent ref + ref, err := reference.Parse(fmt.Sprintf("%s/whodis:9.9.9", suite.DockerRegistryHost)) + suite.Nil(err) + err = suite.RegistryClient.RemoveChart(ref) + suite.NotNil(err) + + // existing ref + ref, err = reference.Parse(path.Join(suite.DockerRegistryHost, "testchart:1.2.3")) + suite.Nil(err) + err = suite.RegistryClient.RemoveChart(ref) + suite.Nil(err) +} + +func TestRegistryClientTestSuite(t *testing.T) { + suite.Run(t, new(RegistryClientTestSuite)) +} + +func (suite *RegistryClientTestSuite) Test_6_FindChart() { + // previous tests saved and pushed this chart + ref, err := reference.Parse(path.Join(suite.DockerRegistryHost, "testchart:1.2.3")) + suite.Nil(err) + + if err := suite.RegistryClient.FindChart(ref); err != nil { + suite.T().Error(err) + } +} + +func (suite *RegistryClientTestSuite) Test_7_FindChartError() { + ref, err := reference.Parse("someserver.local/something/nginx") + suite.Nil(err) + + ref2, err := reference.Parse(path.Join(suite.DockerRegistryHost, "nginx1")) + suite.Nil(err) + + err = suite.RegistryClient.FindChart(ref) + if err == nil { + suite.T().Errorf("Expected error for bad chart URL, but did not get any errors") + } + if err != nil && err.Error() != `chart "someserver.local/something/nginx" not found in repository` { + suite.T().Errorf("Expected error for bad chart URL, but got a different error (%v)", err) + } + + err = suite.RegistryClient.FindChart(ref2) + if err == nil { + suite.T().Errorf("Expected error for chart not found, but did not get any errors") + } + if err != nil && err.Error() != `chart "`+suite.DockerRegistryHost+`/nginx1" not found in repository` { + suite.T().Errorf("Expected error for chart not found, but got a different error (%v)", err) + } +} + +func (suite *RegistryClientTestSuite) Test_8_FetchTags() { + tags, err := suite.RegistryClient.FetchTags(path.Join(suite.DockerRegistryHost, "testchart")) + suite.Nil(err) + fmt.Println(tags) + suite.ElementsMatch(tags, []string{"1.2.3", "4.5.6"}) +} diff --git a/pkg/registry/constants.go b/pkg/repo/constants.go similarity index 96% rename from pkg/registry/constants.go rename to pkg/repo/constants.go index 2883815e7..1361ca79b 100644 --- a/pkg/registry/constants.go +++ b/pkg/repo/constants.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry // import "helm.sh/helm/pkg/registry" +package repo // import "helm.sh/helm/pkg/repo" const ( // HelmChartDefaultTag is the default tag used when storing a chart reference with no tag diff --git a/pkg/registry/constants_test.go b/pkg/repo/constants_test.go similarity index 97% rename from pkg/registry/constants_test.go rename to pkg/repo/constants_test.go index 046f7b730..305cd548f 100644 --- a/pkg/registry/constants_test.go +++ b/pkg/repo/constants_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry +package repo import ( "testing" diff --git a/pkg/repo/doc.go b/pkg/repo/doc.go index 19ccf267c..7f80f0f12 100644 --- a/pkg/repo/doc.go +++ b/pkg/repo/doc.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package repo implements the Helm Chart Repository. +/*package repo implements the Helm Chart Repository. A chart repository is an HTTP server that provides information on charts. A local repository cache is an on-disk representation of a chart repository. diff --git a/pkg/repo/fetcher.go b/pkg/repo/fetcher.go new file mode 100644 index 000000000..a3ed7ba40 --- /dev/null +++ b/pkg/repo/fetcher.go @@ -0,0 +1,79 @@ +/* +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 repo + +import ( + "context" + "io" + "net/http" + "path" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/log" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +type tagsResponse struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} + +type tagFetcher struct { + *baseResolver +} + +func (f tagFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.ReadCloser, error) { + ctx = log.WithLogger(ctx, log.G(ctx).WithFields( + logrus.Fields{ + "base": f.base.String(), + "digest": desc.Digest, + }, + )) + + url := f.url(path.Join("tags", "list")) + + return f.open(ctx, url) +} + +func (f tagFetcher) open(ctx context.Context, u string) (io.ReadCloser, error) { + req, err := http.NewRequest(http.MethodGet, u, nil) + if err != nil { + return nil, err + } + + resp, err := f.doRequestWithRetries(ctx, req, nil) + if err != nil { + return nil, err + } + + if resp.StatusCode > 299 { + // TODO(stevvooe): When doing a offset specific request, we should + // really distinguish between a 206 and a 200. In the case of 200, we + // can discard the bytes, hiding the seek behavior from the + // implementation. + + resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return nil, errors.Wrapf(errdefs.ErrNotFound, "content at %v not found", u) + } + return nil, errors.Errorf("unexpected status code %v: %v", u, resp.Status) + } + + return resp.Body, nil +} diff --git a/pkg/repo/index.go b/pkg/repo/index.go deleted file mode 100644 index 4a1e71c8e..000000000 --- a/pkg/repo/index.go +++ /dev/null @@ -1,330 +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 repo - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/Masterminds/semver" - "github.com/ghodss/yaml" - "github.com/pkg/errors" - - "helm.sh/helm/pkg/chart" - "helm.sh/helm/pkg/chart/loader" - "helm.sh/helm/pkg/provenance" - "helm.sh/helm/pkg/urlutil" -) - -var indexPath = "index.yaml" - -// APIVersionV1 is the v1 API version for index and repository files. -const APIVersionV1 = "v1" - -var ( - // ErrNoAPIVersion indicates that an API version was not specified. - ErrNoAPIVersion = errors.New("no API version specified") - // ErrNoChartVersion indicates that a chart with the given version is not found. - ErrNoChartVersion = errors.New("no chart version found") - // ErrNoChartName indicates that a chart with the given name is not found. - ErrNoChartName = errors.New("no chart name found") -) - -// ChartVersions is a list of versioned chart references. -// Implements a sorter on Version. -type ChartVersions []*ChartVersion - -// Len returns the length. -func (c ChartVersions) Len() int { return len(c) } - -// Swap swaps the position of two items in the versions slice. -func (c ChartVersions) Swap(i, j int) { c[i], c[j] = c[j], c[i] } - -// Less returns true if the version of entry a is less than the version of entry b. -func (c ChartVersions) Less(a, b int) bool { - // Failed parse pushes to the back. - i, err := semver.NewVersion(c[a].Version) - if err != nil { - return true - } - j, err := semver.NewVersion(c[b].Version) - if err != nil { - return false - } - return i.LessThan(j) -} - -// IndexFile represents the index file in a chart repository -type IndexFile struct { - APIVersion string `json:"apiVersion"` - Generated time.Time `json:"generated"` - Entries map[string]ChartVersions `json:"entries"` - PublicKeys []string `json:"publicKeys,omitempty"` -} - -// NewIndexFile initializes an index. -func NewIndexFile() *IndexFile { - return &IndexFile{ - APIVersion: APIVersionV1, - Generated: time.Now(), - Entries: map[string]ChartVersions{}, - PublicKeys: []string{}, - } -} - -// LoadIndexFile takes a file at the given path and returns an IndexFile object -func LoadIndexFile(path string) (*IndexFile, error) { - b, err := ioutil.ReadFile(path) - if err != nil { - return nil, err - } - return loadIndex(b) -} - -// Add adds a file to the index -// This can leave the index in an unsorted state -func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { - u := filename - if baseURL != "" { - var err error - _, file := filepath.Split(filename) - u, err = urlutil.URLJoin(baseURL, file) - if err != nil { - u = path.Join(baseURL, file) - } - } - cr := &ChartVersion{ - URLs: []string{u}, - Metadata: md, - Digest: digest, - Created: time.Now(), - } - if ee, ok := i.Entries[md.Name]; !ok { - i.Entries[md.Name] = ChartVersions{cr} - } else { - i.Entries[md.Name] = append(ee, cr) - } -} - -// Has returns true if the index has an entry for a chart with the given name and exact version. -func (i IndexFile) Has(name, version string) bool { - _, err := i.Get(name, version) - return err == nil -} - -// SortEntries sorts the entries by version in descending order. -// -// In canonical form, the individual version records should be sorted so that -// the most recent release for every version is in the 0th slot in the -// Entries.ChartVersions array. That way, tooling can predict the newest -// version without needing to parse SemVers. -func (i IndexFile) SortEntries() { - for _, versions := range i.Entries { - sort.Sort(sort.Reverse(versions)) - } -} - -// Get returns the ChartVersion for the given name. -// -// If version is empty, this will return the chart with the highest version. -func (i IndexFile) Get(name, version string) (*ChartVersion, error) { - vs, ok := i.Entries[name] - if !ok { - return nil, ErrNoChartName - } - if len(vs) == 0 { - return nil, ErrNoChartVersion - } - - var constraint *semver.Constraints - if len(version) == 0 { - constraint, _ = semver.NewConstraint("*") - } else { - var err error - constraint, err = semver.NewConstraint(version) - if err != nil { - return nil, err - } - } - - for _, ver := range vs { - test, err := semver.NewVersion(ver.Version) - if err != nil { - continue - } - - if constraint.Check(test) { - return ver, nil - } - } - return nil, errors.Errorf("no chart version found for %s-%s", name, version) -} - -// WriteFile writes an index file to the given destination path. -// -// The mode on the file is set to 'mode'. -func (i IndexFile) WriteFile(dest string, mode os.FileMode) error { - b, err := yaml.Marshal(i) - if err != nil { - return err - } - return ioutil.WriteFile(dest, b, mode) -} - -// Merge merges the given index file into this index. -// -// This merges by name and version. -// -// If one of the entries in the given index does _not_ already exist, it is added. -// In all other cases, the existing record is preserved. -// -// This can leave the index in an unsorted state -func (i *IndexFile) Merge(f *IndexFile) { - for _, cvs := range f.Entries { - for _, cv := range cvs { - if !i.Has(cv.Name, cv.Version) { - e := i.Entries[cv.Name] - i.Entries[cv.Name] = append(e, cv) - } - } - } -} - -// ChartVersion represents a chart entry in the IndexFile -type ChartVersion struct { - *chart.Metadata - URLs []string `json:"urls"` - Created time.Time `json:"created,omitempty"` - Removed bool `json:"removed,omitempty"` - Digest string `json:"digest,omitempty"` -} - -// IndexDirectory reads a (flat) directory and generates an index. -// -// It indexes only charts that have been packaged (*.tgz). -// -// The index returned will be in an unsorted state -func IndexDirectory(dir, baseURL string) (*IndexFile, error) { - archives, err := filepath.Glob(filepath.Join(dir, "*.tgz")) - if err != nil { - return nil, err - } - moreArchives, err := filepath.Glob(filepath.Join(dir, "**/*.tgz")) - if err != nil { - return nil, err - } - archives = append(archives, moreArchives...) - - index := NewIndexFile() - for _, arch := range archives { - fname, err := filepath.Rel(dir, arch) - if err != nil { - return index, err - } - - var parentDir string - parentDir, fname = filepath.Split(fname) - // filepath.Split appends an extra slash to the end of parentDir. We want to strip that out. - parentDir = strings.TrimSuffix(parentDir, string(os.PathSeparator)) - parentURL, err := urlutil.URLJoin(baseURL, parentDir) - if err != nil { - parentURL = path.Join(baseURL, parentDir) - } - - c, err := loader.Load(arch) - if err != nil { - // Assume this is not a chart. - continue - } - hash, err := provenance.DigestFile(arch) - if err != nil { - return index, err - } - index.Add(c.Metadata, fname, parentURL, hash) - } - return index, nil -} - -// loadIndex loads an index file and does minimal validity checking. -// -// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. -func loadIndex(data []byte) (*IndexFile, error) { - i := &IndexFile{} - if err := yaml.Unmarshal(data, i); err != nil { - return i, err - } - i.SortEntries() - if i.APIVersion == "" { - // When we leave Beta, we should remove legacy support and just - // return this error: - //return i, ErrNoAPIVersion - return loadUnversionedIndex(data) - } - return i, nil -} - -// unversionedEntry represents a deprecated pre-Alpha.5 format. -// -// This will be removed prior to v2.0.0 -type unversionedEntry struct { - Checksum string `json:"checksum"` - URL string `json:"url"` - Chartfile *chart.Metadata `json:"chartfile"` -} - -// loadUnversionedIndex loads a pre-Alpha.5 index.yaml file. -// -// This format is deprecated. This function will be removed prior to v2.0.0. -func loadUnversionedIndex(data []byte) (*IndexFile, error) { - fmt.Fprintln(os.Stderr, "WARNING: Deprecated index file format. Try 'helm repo update'") - i := map[string]unversionedEntry{} - - // This gets around an error in the YAML parser. Instead of parsing as YAML, - // we convert to JSON, and then decode again. - var err error - data, err = yaml.YAMLToJSON(data) - if err != nil { - return nil, err - } - if err := json.Unmarshal(data, &i); err != nil { - return nil, err - } - - if len(i) == 0 { - return nil, ErrNoAPIVersion - } - ni := NewIndexFile() - for n, item := range i { - if item.Chartfile == nil || item.Chartfile.Name == "" { - parts := strings.Split(n, "-") - ver := "" - if len(parts) > 1 { - ver = strings.TrimSuffix(parts[1], ".tgz") - } - item.Chartfile = &chart.Metadata{Name: parts[0], Version: ver} - } - ni.Add(item.Chartfile, item.URL, "", item.Checksum) - } - return ni, nil -} diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go deleted file mode 100644 index 329b80b29..000000000 --- a/pkg/repo/index_test.go +++ /dev/null @@ -1,352 +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 repo - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "helm.sh/helm/pkg/chart" - "helm.sh/helm/pkg/cli" - "helm.sh/helm/pkg/getter" -) - -const ( - testfile = "testdata/local-index.yaml" - unorderedTestfile = "testdata/local-index-unordered.yaml" - testRepo = "test-repo" -) - -func TestIndexFile(t *testing.T) { - i := NewIndexFile() - i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") - i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.1"}, "cutter-0.1.1.tgz", "http://example.com/charts", "sha256:1234567890abc") - i.Add(&chart.Metadata{Name: "cutter", Version: "0.1.0"}, "cutter-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890abc") - i.Add(&chart.Metadata{Name: "cutter", Version: "0.2.0"}, "cutter-0.2.0.tgz", "http://example.com/charts", "sha256:1234567890abc") - i.SortEntries() - - if i.APIVersion != APIVersionV1 { - t.Error("Expected API version v1") - } - - if len(i.Entries) != 2 { - t.Errorf("Expected 2 charts. Got %d", len(i.Entries)) - } - - if i.Entries["clipper"][0].Name != "clipper" { - t.Errorf("Expected clipper, got %s", i.Entries["clipper"][0].Name) - } - - if len(i.Entries["cutter"]) != 3 { - t.Error("Expected two cutters.") - } - - // Test that the sort worked. 0.2 should be at the first index for Cutter. - if v := i.Entries["cutter"][0].Version; v != "0.2.0" { - t.Errorf("Unexpected first version: %s", v) - } -} - -func TestLoadIndex(t *testing.T) { - b, err := ioutil.ReadFile(testfile) - if err != nil { - t.Fatal(err) - } - i, err := loadIndex(b) - if err != nil { - t.Fatal(err) - } - verifyLocalIndex(t, i) -} - -func TestLoadIndexFile(t *testing.T) { - i, err := LoadIndexFile(testfile) - if err != nil { - t.Fatal(err) - } - verifyLocalIndex(t, i) -} - -func TestLoadUnorderedIndex(t *testing.T) { - b, err := ioutil.ReadFile(unorderedTestfile) - if err != nil { - t.Fatal(err) - } - i, err := loadIndex(b) - if err != nil { - t.Fatal(err) - } - verifyLocalIndex(t, i) -} - -func TestMerge(t *testing.T) { - ind1 := NewIndexFile() - ind1.Add(&chart.Metadata{ - Name: "dreadnought", - Version: "0.1.0", - }, "dreadnought-0.1.0.tgz", "http://example.com", "aaaa") - - ind2 := NewIndexFile() - ind2.Add(&chart.Metadata{ - Name: "dreadnought", - Version: "0.2.0", - }, "dreadnought-0.2.0.tgz", "http://example.com", "aaaabbbb") - ind2.Add(&chart.Metadata{ - Name: "doughnut", - Version: "0.2.0", - }, "doughnut-0.2.0.tgz", "http://example.com", "ccccbbbb") - - ind1.Merge(ind2) - - if len(ind1.Entries) != 2 { - t.Errorf("Expected 2 entries, got %d", len(ind1.Entries)) - vs := ind1.Entries["dreadnaught"] - if len(vs) != 2 { - t.Errorf("Expected 2 versions, got %d", len(vs)) - } - v := vs[0] - if v.Version != "0.2.0" { - t.Errorf("Expected %q version to be 0.2.0, got %s", v.Name, v.Version) - } - } - -} - -func TestDownloadIndexFile(t *testing.T) { - srv, err := startLocalServerForTests(nil) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - - dirName, err := ioutil.TempDir("", "tmp") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(dirName) - - indexFilePath := filepath.Join(dirName, testRepo+"-index.yaml") - r, err := NewChartRepository(&Entry{ - Name: testRepo, - URL: srv.URL, - Cache: indexFilePath, - }, getter.All(cli.EnvSettings{})) - if err != nil { - t.Errorf("Problem creating chart repository from %s: %v", testRepo, err) - } - - if err := r.DownloadIndexFile(""); err != nil { - t.Errorf("%#v", err) - } - - if _, err := os.Stat(indexFilePath); err != nil { - t.Errorf("error finding created index file: %#v", err) - } - - b, err := ioutil.ReadFile(indexFilePath) - if err != nil { - t.Errorf("error reading index file: %#v", err) - } - - i, err := loadIndex(b) - if err != nil { - t.Errorf("Index %q failed to parse: %s", testfile, err) - return - } - - verifyLocalIndex(t, i) -} - -func verifyLocalIndex(t *testing.T, i *IndexFile) { - numEntries := len(i.Entries) - if numEntries != 3 { - t.Errorf("Expected 3 entries in index file but got %d", numEntries) - } - - alpine, ok := i.Entries["alpine"] - if !ok { - t.Errorf("'alpine' section not found.") - return - } - - if l := len(alpine); l != 1 { - t.Errorf("'alpine' should have 1 chart, got %d", l) - return - } - - nginx, ok := i.Entries["nginx"] - if !ok || len(nginx) != 2 { - t.Error("Expected 2 nginx entries") - return - } - - expects := []*ChartVersion{ - { - Metadata: &chart.Metadata{ - Name: "alpine", - Description: "string", - Version: "1.0.0", - Keywords: []string{"linux", "alpine", "small", "sumtin"}, - Home: "https://github.com/something", - }, - URLs: []string{ - "https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz", - "http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz", - }, - Digest: "sha256:1234567890abcdef", - }, - { - Metadata: &chart.Metadata{ - Name: "nginx", - Description: "string", - Version: "0.2.0", - Keywords: []string{"popular", "web server", "proxy"}, - Home: "https://github.com/something/else", - }, - URLs: []string{ - "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz", - }, - Digest: "sha256:1234567890abcdef", - }, - { - Metadata: &chart.Metadata{ - Name: "nginx", - Description: "string", - Version: "0.1.0", - Keywords: []string{"popular", "web server", "proxy"}, - Home: "https://github.com/something", - }, - URLs: []string{ - "https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz", - }, - Digest: "sha256:1234567890abcdef", - }, - } - tests := []*ChartVersion{alpine[0], nginx[0], nginx[1]} - - for i, tt := range tests { - expect := expects[i] - if tt.Name != expect.Name { - t.Errorf("Expected name %q, got %q", expect.Name, tt.Name) - } - if tt.Description != expect.Description { - t.Errorf("Expected description %q, got %q", expect.Description, tt.Description) - } - if tt.Version != expect.Version { - t.Errorf("Expected version %q, got %q", expect.Version, tt.Version) - } - if tt.Digest != expect.Digest { - t.Errorf("Expected digest %q, got %q", expect.Digest, tt.Digest) - } - if tt.Home != expect.Home { - t.Errorf("Expected home %q, got %q", expect.Home, tt.Home) - } - - for i, url := range tt.URLs { - if url != expect.URLs[i] { - t.Errorf("Expected URL %q, got %q", expect.URLs[i], url) - } - } - for i, kw := range tt.Keywords { - if kw != expect.Keywords[i] { - t.Errorf("Expected keywords %q, got %q", expect.Keywords[i], kw) - } - } - } -} - -func TestIndexDirectory(t *testing.T) { - dir := "testdata/repository" - index, err := IndexDirectory(dir, "http://localhost:8080") - if err != nil { - t.Fatal(err) - } - - if l := len(index.Entries); l != 3 { - t.Fatalf("Expected 3 entries, got %d", l) - } - - // Other things test the entry generation more thoroughly. We just test a - // few fields. - - corpus := []struct{ chartName, downloadLink string }{ - {"frobnitz", "http://localhost:8080/frobnitz-1.2.3.tgz"}, - {"zarthal", "http://localhost:8080/universe/zarthal-1.0.0.tgz"}, - } - - for _, test := range corpus { - cname := test.chartName - frobs, ok := index.Entries[cname] - if !ok { - t.Fatalf("Could not read chart %s", cname) - } - - frob := frobs[0] - if len(frob.Digest) == 0 { - t.Errorf("Missing digest of file %s.", frob.Name) - } - if frob.URLs[0] != test.downloadLink { - t.Errorf("Unexpected URLs: %v", frob.URLs) - } - if frob.Name != cname { - t.Errorf("Expected %q, got %q", cname, frob.Name) - } - } -} - -func TestLoadUnversionedIndex(t *testing.T) { - data, err := ioutil.ReadFile("testdata/unversioned-index.yaml") - if err != nil { - t.Fatal(err) - } - - ind, err := loadUnversionedIndex(data) - if err != nil { - t.Fatal(err) - } - - if l := len(ind.Entries); l != 2 { - t.Fatalf("Expected 2 entries, got %d", l) - } - - if l := len(ind.Entries["mysql"]); l != 3 { - t.Fatalf("Expected 3 mysql versions, got %d", l) - } -} - -func TestIndexAdd(t *testing.T) { - i := NewIndexFile() - i.Add(&chart.Metadata{Name: "clipper", Version: "0.1.0"}, "clipper-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") - - if i.Entries["clipper"][0].URLs[0] != "http://example.com/charts/clipper-0.1.0.tgz" { - t.Errorf("Expected http://example.com/charts/clipper-0.1.0.tgz, got %s", i.Entries["clipper"][0].URLs[0]) - } - - i.Add(&chart.Metadata{Name: "alpine", Version: "0.1.0"}, "/home/charts/alpine-0.1.0.tgz", "http://example.com/charts", "sha256:1234567890") - - if i.Entries["alpine"][0].URLs[0] != "http://example.com/charts/alpine-0.1.0.tgz" { - t.Errorf("Expected http://example.com/charts/alpine-0.1.0.tgz, got %s", i.Entries["alpine"][0].URLs[0]) - } - - i.Add(&chart.Metadata{Name: "deis", Version: "0.1.0"}, "/home/charts/deis-0.1.0.tgz", "http://example.com/charts/", "sha256:1234567890") - - if i.Entries["deis"][0].URLs[0] != "http://example.com/charts/deis-0.1.0.tgz" { - t.Errorf("Expected http://example.com/charts/deis-0.1.0.tgz, got %s", i.Entries["deis"][0].URLs[0]) - } -} diff --git a/pkg/registry/resolver.go b/pkg/repo/reference.go similarity index 66% rename from pkg/registry/resolver.go rename to pkg/repo/reference.go index fce303c73..f5c7caea0 100644 --- a/pkg/registry/resolver.go +++ b/pkg/repo/reference.go @@ -14,15 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry // import "helm.sh/helm/pkg/registry" +package repo // import "helm.sh/helm/pkg/repo" import ( - "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/reference" ) -type ( - // Resolver provides remotes based on a locator - Resolver struct { - remotes.Resolver +// ParseNameTag converts a name and a version to a reference +func ParseNameTag(name, tag string) (reference.Spec, error) { + s := name + if tag != "" { + s += ":" + tag } -) + return reference.Parse(s) +} diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go deleted file mode 100644 index 8bfe0115b..000000000 --- a/pkg/repo/repo.go +++ /dev/null @@ -1,145 +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 repo // import "helm.sh/helm/pkg/repo" - -import ( - "fmt" - "io/ioutil" - "os" - "time" - - "github.com/ghodss/yaml" - "github.com/pkg/errors" -) - -// ErrRepoOutOfDate indicates that the repository file is out of date, but -// is fixable. -var ErrRepoOutOfDate = errors.New("repository file is out of date") - -// File represents the repositories.yaml file in $HELM_HOME -type File struct { - APIVersion string `json:"apiVersion"` - Generated time.Time `json:"generated"` - Repositories []*Entry `json:"repositories"` -} - -// NewFile generates an empty repositories file. -// -// Generated and APIVersion are automatically set. -func NewFile() *File { - return &File{ - APIVersion: APIVersionV1, - Generated: time.Now(), - Repositories: []*Entry{}, - } -} - -// LoadFile takes a file at the given path and returns a File object -// -// If this returns ErrRepoOutOfDate, it also returns a recovered File that -// can be saved as a replacement to the out of date file. -func LoadFile(path string) (*File, error) { - b, err := ioutil.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return nil, errors.Errorf("couldn't load repositories file (%s).\nYou might need to run `helm init`", path) - } - return nil, err - } - - r := &File{} - err = yaml.Unmarshal(b, r) - if err != nil { - return nil, err - } - - // File is either corrupt, or is from before v2.0.0-Alpha.5 - if r.APIVersion == "" { - m := map[string]string{} - if err = yaml.Unmarshal(b, &m); err != nil { - return nil, err - } - r := NewFile() - for k, v := range m { - r.Add(&Entry{ - Name: k, - URL: v, - Cache: fmt.Sprintf("%s-index.yaml", k), - }) - } - return r, ErrRepoOutOfDate - } - - return r, nil -} - -// Add adds one or more repo entries to a repo file. -func (r *File) Add(re ...*Entry) { - r.Repositories = append(r.Repositories, re...) -} - -// Update attempts to replace one or more repo entries in a repo file. If an -// entry with the same name doesn't exist in the repo file it will add it. -func (r *File) Update(re ...*Entry) { - for _, target := range re { - found := false - for j, repo := range r.Repositories { - if repo.Name == target.Name { - r.Repositories[j] = target - found = true - break - } - } - if !found { - r.Add(target) - } - } -} - -// Has returns true if the given name is already a repository name. -func (r *File) Has(name string) bool { - for _, rf := range r.Repositories { - if rf.Name == name { - return true - } - } - return false -} - -// Remove removes the entry from the list of repositories. -func (r *File) Remove(name string) bool { - cp := []*Entry{} - found := false - for _, rf := range r.Repositories { - if rf.Name == name { - found = true - continue - } - cp = append(cp, rf) - } - r.Repositories = cp - return found -} - -// WriteFile writes a repositories file to the given path. -func (r *File) WriteFile(path string, perm os.FileMode) error { - data, err := yaml.Marshal(r) - if err != nil { - return err - } - return ioutil.WriteFile(path, data, perm) -} diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go deleted file mode 100644 index acec29bcc..000000000 --- a/pkg/repo/repo_test.go +++ /dev/null @@ -1,227 +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 repo - -import "testing" -import "io/ioutil" -import "os" -import "strings" - -const testRepositoriesFile = "testdata/repositories.yaml" - -func TestFile(t *testing.T) { - rf := NewFile() - rf.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", - }, - ) - - if len(rf.Repositories) != 2 { - t.Fatal("Expected 2 repositories") - } - - if rf.Has("nosuchrepo") { - t.Error("Found nonexistent repo") - } - if !rf.Has("incubator") { - t.Error("incubator repo is missing") - } - - stable := rf.Repositories[0] - if stable.Name != "stable" { - t.Error("stable is not named stable") - } - if stable.URL != "https://example.com/stable/charts" { - t.Error("Wrong URL for stable") - } - if stable.Cache != "stable-index.yaml" { - t.Error("Wrong cache name for stable") - } -} - -func TestNewFile(t *testing.T) { - expects := NewFile() - expects.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", - }, - ) - - file, err := LoadFile(testRepositoriesFile) - if err != nil { - t.Errorf("%q could not be loaded: %s", testRepositoriesFile, err) - } - - if len(expects.Repositories) != len(file.Repositories) { - t.Fatalf("Unexpected repo data: %#v", file.Repositories) - } - - for i, expect := range expects.Repositories { - got := file.Repositories[i] - if expect.Name != got.Name { - t.Errorf("Expected name %q, got %q", expect.Name, got.Name) - } - if expect.URL != got.URL { - t.Errorf("Expected url %q, got %q", expect.URL, got.URL) - } - if expect.Cache != got.Cache { - t.Errorf("Expected cache %q, got %q", expect.Cache, got.Cache) - } - } -} - -func TestNewPreV1File(t *testing.T) { - r, err := LoadFile("testdata/old-repositories.yaml") - if err != nil && err != ErrRepoOutOfDate { - t.Fatal(err) - } - if len(r.Repositories) != 3 { - t.Fatalf("Expected 3 repos: %#v", r) - } - - // Because they are parsed as a map, we lose ordering. - found := false - for _, rr := range r.Repositories { - if rr.Name == "best-charts-ever" { - found = true - } - } - if !found { - t.Errorf("expected the best charts ever. Got %#v", r.Repositories) - } -} - -func TestRemoveRepository(t *testing.T) { - sampleRepository := NewFile() - sampleRepository.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", - }, - ) - - removeRepository := "stable" - found := sampleRepository.Remove(removeRepository) - if !found { - t.Errorf("expected repository %s not found", removeRepository) - } - - found = sampleRepository.Has(removeRepository) - if found { - t.Errorf("repository %s not deleted", removeRepository) - } -} - -func TestUpdateRepository(t *testing.T) { - sampleRepository := NewFile() - sampleRepository.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", - }, - ) - newRepoName := "sample" - sampleRepository.Update(&Entry{Name: newRepoName, - URL: "https://example.com/sample", - Cache: "sample-index.yaml", - }) - - if !sampleRepository.Has(newRepoName) { - t.Errorf("expected repository %s not found", newRepoName) - } - repoCount := len(sampleRepository.Repositories) - - sampleRepository.Update(&Entry{Name: newRepoName, - URL: "https://example.com/sample", - Cache: "sample-index.yaml", - }) - - if repoCount != len(sampleRepository.Repositories) { - t.Errorf("invalid number of repositories found %d, expected number of repositories %d", len(sampleRepository.Repositories), repoCount) - } -} - -func TestWriteFile(t *testing.T) { - sampleRepository := NewFile() - sampleRepository.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - Cache: "stable-index.yaml", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - Cache: "incubator-index.yaml", - }, - ) - - file, err := ioutil.TempFile("", "helm-repo") - if err != nil { - t.Errorf("failed to create test-file (%v)", err) - } - defer os.Remove(file.Name()) - if err := sampleRepository.WriteFile(file.Name(), 0744); err != nil { - t.Errorf("failed to write file (%v)", err) - } - - repos, err := LoadFile(file.Name()) - if err != nil { - t.Errorf("failed to load file (%v)", err) - } - for _, repo := range sampleRepository.Repositories { - if !repos.Has(repo.Name) { - t.Errorf("expected repository %s not found", repo.Name) - } - } -} - -func TestRepoNotExists(t *testing.T) { - _, err := LoadFile("/this/path/does/not/exist.yaml") - if err == nil { - t.Errorf("expected err to be non-nil when path does not exist") - } else if !strings.Contains(err.Error(), "You might need to run `helm init`") { - t.Errorf("expected prompt to run `helm init` when repositories file does not exist") - } -} diff --git a/pkg/repo/repotest/doc.go b/pkg/repo/repotest/doc.go index 3bf98aa7e..b224aba06 100644 --- a/pkg/repo/repotest/doc.go +++ b/pkg/repo/repotest/doc.go @@ -1,10 +1,11 @@ /* 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, @@ -13,8 +14,5 @@ See the License for the specific language governing permissions and limitations under the License. */ -/*Package repotest provides utilities for testing. - -The server provides a testing server that can be set up and torn down quickly. -*/ +// package repotest provides utilities for testing against OCI registries. package repotest diff --git a/pkg/repo/repotest/repotest.go b/pkg/repo/repotest/repotest.go new file mode 100644 index 000000000..f0ff90c6d --- /dev/null +++ b/pkg/repo/repotest/repotest.go @@ -0,0 +1,134 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repotest // import "helm.sh/helm/pkg/repo/repotest" + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" + + // register inmemory driver for testing + _ "github.com/docker/distribution/registry/storage/driver/inmemory" + + "github.com/docker/distribution/configuration" + "github.com/docker/distribution/registry/handlers" + "github.com/docker/distribution/registry/listener" +) + +type Server struct { + Config *configuration.Configuration + + ln net.Listener + server *http.Server + url string +} + +// NewServer returns a new Server and starts it. +// +// The caller should call Close or Shutdown when finished to shut it down. +func NewServer() *Server { + s := NewUnstartedServer() + s.Start() + return s +} + +// NewUnstartedServer returns a new Server but doesn't start it. +// +// After changing its configuration, the caller should call Start. +// +// The caller should call Close or Shutdown when finished to shut it down. +func NewUnstartedServer() *Server { + config := &configuration.Configuration{ + Version: "0.1", + Log: struct { + AccessLog struct { + Disabled bool `yaml:"disabled,omitempty"` + } `yaml:"accesslog,omitempty"` + Level configuration.Loglevel `yaml:"level,omitempty"` + Formatter string `yaml:"formatter,omitempty"` + Fields map[string]interface{} `yaml:"fields,omitempty"` + Hooks []configuration.LogHook `yaml:"hooks,omitempty"` + }{ + Level: configuration.Loglevel("error"), + }, + Storage: configuration.Storage{ + "inmemory": configuration.Parameters{}, + }, + } + config.HTTP.Secret = "registrytest" + + app := handlers.NewApp(context.Background(), config) + app.RegisterHealthChecks() + + server := &http.Server{ + Handler: app, + } + + return &Server{ + Config: config, + server: server, + } +} + +// Start starts a server from NewUnstartedServer. +func (s *Server) Start() { + if s.url != "" { + panic("server already started") + } + + // only support localhost TCP listener addresses so we can determine the URL + // TODO: support changes to s.Config.HTTP.Addr + ln, err := listener.NewListener("", "") + if err != nil { + panic(err) + } + s.ln = ln + s.url = fmt.Sprintf("%s://%s", s.ln.Addr().Network(), s.ln.Addr().String()) + + // Start serving in goroutine and listen for stop signal from Stop or Shutdown + go s.server.Serve(s.ln) +} + +// URL returns the server's network address. This is compatible with Docker's URL syntax so it can be used +// in conjunction with reference.Parse +func (s *Server) URL() string { + if s.url == "" { + return "" + } + return fmt.Sprintf("localhost:%s", strings.TrimPrefix(s.url, "tcp://[::]:")) +} + +// Close immediately closes all active net.Listeners and any active client connections. For a graceful shutdown, use Shutdown. +// +// Close returns any error returned from closing the Server's underlying Listener(s). +func (s *Server) Close() error { + s.url = "" + return s.server.Close() +} + +// Shutdown shuts down the server and blocks until all outstanding requests on this server have completed. +// +// When Shutdown is called, Start immediately returns http.ErrServerClosed. Make sure the program doesn't exit and waits instead for Shutdown to return. +func (s *Server) Shutdown() error { + s.url = "" + // shutdown the server with a grace period of configured timeout + c, cancel := context.WithTimeout(context.Background(), s.Config.HTTP.DrainTimeout) + defer cancel() + return s.server.Shutdown(c) +} diff --git a/pkg/repo/repotest/repotest_test.go b/pkg/repo/repotest/repotest_test.go new file mode 100644 index 000000000..5788b3f05 --- /dev/null +++ b/pkg/repo/repotest/repotest_test.go @@ -0,0 +1,87 @@ +/* +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 repotest + +import ( + "strings" + "testing" +) + +func TestNewServer(t *testing.T) { + s := NewServer() + + if err := s.Shutdown(); err != nil { + t.Error(err) + } + + // restart to test Close + s.Start() + + if err := s.Close(); err != nil { + t.Error(err) + } +} + +func TestNewServerStart(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("did not recover when calling Start on a Server that already started") + } + }() + + NewServer().Start() +} + +func TestUnstartedServerDoubleStart(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("did not recover when calling Start on a Server that already started") + } + }() + + s := NewUnstartedServer() + s.Start() + s.Start() +} + +func TestURL(t *testing.T) { + s := NewUnstartedServer() + + expected := "" + actual := s.URL() + if expected != actual { + t.Errorf("expected '%s, got '%s'", expected, actual) + } + + s.Start() + + if !strings.HasPrefix(s.URL(), "localhost:") { + t.Errorf("expected url to start with 'localhost:', got '%s'", s.URL()) + } + + s.Close() + + // test that we ignore this parameter + s.Config.HTTP.Net = "unix" + + s.Start() + defer s.Close() + + if !strings.HasPrefix(s.URL(), "localhost:") { + t.Errorf("expected url to start with 'localhost:', got '%s'", s.URL()) + } +} diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go deleted file mode 100644 index 72688e63c..000000000 --- a/pkg/repo/repotest/server.go +++ /dev/null @@ -1,172 +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 repotest - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - - "github.com/ghodss/yaml" - - "helm.sh/helm/pkg/helmpath" - "helm.sh/helm/pkg/repo" -) - -// NewTempServer creates a server inside of a temp dir. -// -// If the passed in string is not "", it will be treated as a shell glob, and files -// will be copied from that path to the server's docroot. -// -// The caller is responsible for destroying the temp directory as well as stopping -// the server. -func NewTempServer(glob string) (*Server, helmpath.Home, error) { - tdir, err := ioutil.TempDir("", "helm-repotest-") - tdirh := helmpath.Home(tdir) - if err != nil { - return nil, tdirh, err - } - srv := NewServer(tdir) - - if glob != "" { - if _, err := srv.CopyCharts(glob); err != nil { - srv.Stop() - return srv, tdirh, err - } - } - - return srv, tdirh, nil -} - -// NewServer creates a repository server for testing. -// -// docroot should be a temp dir managed by the caller. -// -// This will start the server, serving files off of the docroot. -// -// Use CopyCharts to move charts into the repository and then index them -// for service. -func NewServer(docroot string) *Server { - root, err := filepath.Abs(docroot) - if err != nil { - panic(err) - } - srv := &Server{ - docroot: root, - } - srv.start() - // Add the testing repository as the only repo. - if err := setTestingRepository(helmpath.Home(docroot), "test", srv.URL()); err != nil { - panic(err) - } - return srv -} - -// Server is an implementation of a repository server for testing. -type Server struct { - docroot string - srv *httptest.Server -} - -// Root gets the docroot for the server. -func (s *Server) Root() string { - return s.docroot -} - -// CopyCharts takes a glob expression and copies those charts to the server root. -func (s *Server) CopyCharts(origin string) ([]string, error) { - files, err := filepath.Glob(origin) - if err != nil { - return []string{}, err - } - copied := make([]string, len(files)) - for i, f := range files { - base := filepath.Base(f) - newname := filepath.Join(s.docroot, base) - data, err := ioutil.ReadFile(f) - if err != nil { - return []string{}, err - } - if err := ioutil.WriteFile(newname, data, 0755); err != nil { - return []string{}, err - } - copied[i] = newname - } - - err = s.CreateIndex() - return copied, err -} - -// CreateIndex will read docroot and generate an index.yaml file. -func (s *Server) CreateIndex() error { - // generate the index - index, err := repo.IndexDirectory(s.docroot, s.URL()) - if err != nil { - return err - } - - d, err := yaml.Marshal(index) - if err != nil { - return err - } - - ifile := filepath.Join(s.docroot, "index.yaml") - return ioutil.WriteFile(ifile, d, 0755) -} - -func (s *Server) start() { - s.srv = httptest.NewServer(http.FileServer(http.Dir(s.docroot))) -} - -// Stop stops the server and closes all connections. -// -// It should be called explicitly. -func (s *Server) Stop() { - s.srv.Close() -} - -// URL returns the URL of the server. -// -// Example: -// http://localhost:1776 -func (s *Server) URL() string { - return s.srv.URL -} - -// LinkIndices links the index created with CreateIndex and makes a symboic link to the repositories/cache directory. -// -// This makes it possible to simulate a local cache of a repository. -func (s *Server) LinkIndices() error { - destfile := "test-index.yaml" - // Link the index.yaml file to the - lstart := filepath.Join(s.docroot, "index.yaml") - ldest := filepath.Join(s.docroot, "repository/cache", destfile) - return os.Symlink(lstart, ldest) -} - -// setTestingRepository sets up a testing repository.yaml with only the given name/URL. -func setTestingRepository(home helmpath.Home, name, url string) error { - r := repo.NewFile() - r.Add(&repo.Entry{ - Name: name, - URL: url, - Cache: home.CacheIndex(name), - }) - os.MkdirAll(filepath.Join(home.Repository(), name), 0755) - return r.WriteFile(home.RepositoryFile(), 0644) -} diff --git a/pkg/repo/repotest/server_test.go b/pkg/repo/repotest/server_test.go deleted file mode 100644 index 0fc33e252..000000000 --- a/pkg/repo/repotest/server_test.go +++ /dev/null @@ -1,127 +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 repotest - -import ( - "io/ioutil" - "net/http" - "os" - "path/filepath" - "testing" - - "github.com/ghodss/yaml" - - "helm.sh/helm/pkg/repo" -) - -// Young'n, in these here parts, we test our tests. - -func TestServer(t *testing.T) { - docroot, err := ioutil.TempDir("", "helm-repotest-") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(docroot) - - srv := NewServer(docroot) - defer srv.Stop() - - c, err := srv.CopyCharts("testdata/*.tgz") - if err != nil { - // Some versions of Go don't correctly fire defer on Fatal. - t.Error(err) - return - } - - if len(c) != 1 { - t.Errorf("Unexpected chart count: %d", len(c)) - } - - if filepath.Base(c[0]) != "examplechart-0.1.0.tgz" { - t.Errorf("Unexpected chart: %s", c[0]) - } - - res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz") - if err != nil { - t.Error(err) - return - } - - if res.ContentLength < 500 { - t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) - } - - res, err = http.Get(srv.URL() + "/index.yaml") - if err != nil { - t.Error(err) - return - } - - data, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Error(err) - return - } - - m := repo.NewIndexFile() - if err := yaml.Unmarshal(data, m); err != nil { - t.Error(err) - return - } - - if l := len(m.Entries); l != 1 { - t.Errorf("Expected 1 entry, got %d", l) - return - } - - expect := "examplechart" - if !m.Has(expect, "0.1.0") { - t.Errorf("missing %q", expect) - } - - res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") - if err != nil { - t.Error(err) - return - } - if res.StatusCode != 404 { - t.Errorf("Expected 404, got %d", res.StatusCode) - } -} - -func TestNewTempServer(t *testing.T) { - srv, tdir, err := NewTempServer("testdata/examplechart-0.1.0.tgz") - if err != nil { - t.Fatal(err) - } - defer func() { - srv.Stop() - os.RemoveAll(tdir.String()) - }() - - if _, err := os.Stat(tdir.String()); err != nil { - t.Fatal(err) - } - - res, err := http.Head(srv.URL() + "/examplechart-0.1.0.tgz") - if err != nil { - t.Error(err) - } - if res.StatusCode != 200 { - t.Errorf("Expected 200, got %d", res.StatusCode) - } -} diff --git a/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz b/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz deleted file mode 100644 index aec86c640..000000000 Binary files a/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz and /dev/null differ diff --git a/pkg/repo/repotest/testdata/examplechart/.helmignore b/pkg/repo/repotest/testdata/examplechart/.helmignore deleted file mode 100644 index f0c131944..000000000 --- a/pkg/repo/repotest/testdata/examplechart/.helmignore +++ /dev/null @@ -1,21 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*~ -# Various IDEs -.project -.idea/ -*.tmproj diff --git a/pkg/repo/repotest/testdata/examplechart/Chart.yaml b/pkg/repo/repotest/testdata/examplechart/Chart.yaml deleted file mode 100644 index 8e06de648..000000000 --- a/pkg/repo/repotest/testdata/examplechart/Chart.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: A Helm chart for Kubernetes -name: examplechart -version: 0.1.0 diff --git a/pkg/repo/repotest/testdata/examplechart/values.yaml b/pkg/repo/repotest/testdata/examplechart/values.yaml deleted file mode 100644 index 5170c61e3..000000000 --- a/pkg/repo/repotest/testdata/examplechart/values.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# Default values for examplechart. -# This is a YAML-formatted file. -# Declare name/value pairs to be passed into your templates. -# name: value diff --git a/pkg/repo/resolver.go b/pkg/repo/resolver.go new file mode 100644 index 000000000..21fdfa3fe --- /dev/null +++ b/pkg/repo/resolver.go @@ -0,0 +1,214 @@ +/* +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 repo // import "helm.sh/helm/pkg/repo" + +import ( + "context" + "net/http" + "net/url" + "path" + "strings" + + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/reference" + "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/remotes/docker" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/net/context/ctxhttp" +) + +var _ remotes.Resolver = &Resolver{} + +// Resolver provides remotes based on a locator +type Resolver struct { + auth docker.Authorizer + host func(string) (string, error) + plainHTTP bool + client *http.Client + dockerResolver remotes.Resolver +} + +// newResolver returns a new resolver to a Docker registry. +// +// We are simply wrapping its functionality to provide a way to fetch tag lists (see tagFetcher) +func newResolver(options docker.ResolverOptions) *Resolver { + if options.Tracker == nil { + options.Tracker = docker.NewInMemoryTracker() + } + if options.Host == nil { + options.Host = docker.DefaultHost + } + if options.Authorizer == nil { + options.Authorizer = docker.NewAuthorizer(options.Client, options.Credentials) + } + return &Resolver{ + auth: options.Authorizer, + host: options.Host, + plainHTTP: options.PlainHTTP, + client: options.Client, + dockerResolver: docker.NewResolver(options), + } +} + +func (r *Resolver) Resolve(ctx context.Context, ref string) (string, ocispec.Descriptor, error) { + return r.dockerResolver.Resolve(ctx, ref) +} + +func (r *Resolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { + return r.dockerResolver.Fetcher(ctx, ref) +} + +func (r *Resolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) { + return r.dockerResolver.Pusher(ctx, ref) +} + +func (r *Resolver) base(refspec reference.Spec) (*baseResolver, error) { + var ( + err error + base url.URL + ) + + host := refspec.Hostname() + base.Host = host + if r.host != nil { + base.Host, err = r.host(host) + if err != nil { + return nil, err + } + } + + base.Scheme = "https" + if r.plainHTTP || strings.HasPrefix(base.Host, "localhost:") { + base.Scheme = "http" + } + + prefix := strings.TrimPrefix(refspec.Locator, host+"/") + base.Path = path.Join("/v2", prefix) + + return &baseResolver{ + refspec: refspec, + base: base, + client: r.client, + auth: r.auth, + }, nil +} + +type baseResolver struct { + refspec reference.Spec + base url.URL + + client *http.Client + auth docker.Authorizer +} + +func (b *baseResolver) url(ps ...string) string { + url := b.base + url.Path = path.Join(url.Path, path.Join(ps...)) + return url.String() +} + +func (b *baseResolver) authorize(ctx context.Context, req *http.Request) error { + // Check if has header for host + if b.auth != nil { + if err := b.auth.Authorize(ctx, req); err != nil { + return err + } + } + + return nil +} + +func (b *baseResolver) doRequest(ctx context.Context, req *http.Request) (*http.Response, error) { + ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", req.URL.String())) + log.G(ctx).WithField("request.headers", req.Header).WithField("request.method", req.Method).Debug("do request") + if err := b.authorize(ctx, req); err != nil { + return nil, errors.Wrap(err, "failed to authorize") + } + resp, err := ctxhttp.Do(ctx, b.client, req) + if err != nil { + return nil, errors.Wrap(err, "failed to do request") + } + log.G(ctx).WithFields(logrus.Fields{ + "status": resp.Status, + "response.headers": resp.Header, + }).Debug("fetch response received") + return resp, nil +} + +func (b *baseResolver) doRequestWithRetries(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Response, error) { + resp, err := b.doRequest(ctx, req) + if err != nil { + return nil, err + } + + responses = append(responses, resp) + req, err = b.retryRequest(ctx, req, responses) + if err != nil { + resp.Body.Close() + return nil, err + } + if req != nil { + resp.Body.Close() + return b.doRequestWithRetries(ctx, req, responses) + } + return resp, err +} + +func (b *baseResolver) retryRequest(ctx context.Context, req *http.Request, responses []*http.Response) (*http.Request, error) { + if len(responses) > 5 { + return nil, nil + } + last := responses[len(responses)-1] + if last.StatusCode == http.StatusUnauthorized { + log.G(ctx).WithField("header", last.Header.Get("WWW-Authenticate")).Debug("Unauthorized") + if b.auth != nil { + if err := b.auth.AddResponses(ctx, responses); err == nil { + return copyRequest(req) + } else if !errdefs.IsNotImplemented(err) { + return nil, err + } + } + + return nil, nil + } else if last.StatusCode == http.StatusMethodNotAllowed && req.Method == http.MethodHead { + // Support registries which have not properly implemented the HEAD method for + // manifests endpoint + if strings.Contains(req.URL.Path, "/manifests/") { + // TODO: copy request? + req.Method = http.MethodGet + return copyRequest(req) + } + } + + // TODO: Handle 50x errors accounting for attempt history + return nil, nil +} + +func copyRequest(req *http.Request) (*http.Request, error) { + ireq := *req + if ireq.GetBody != nil { + var err error + ireq.Body, err = ireq.GetBody() + if err != nil { + return nil, err + } + } + return &ireq, nil +} diff --git a/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz b/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz index fc8cacec2..8731dce02 100644 Binary files a/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz and b/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz differ diff --git a/pkg/repo/testdata/repository/sprocket-1.1.0.tgz b/pkg/repo/testdata/repository/sprocket-1.1.0.tgz index 595e9cc03..48d65f491 100644 Binary files a/pkg/repo/testdata/repository/sprocket-1.1.0.tgz and b/pkg/repo/testdata/repository/sprocket-1.1.0.tgz differ diff --git a/pkg/repo/testdata/repository/sprocket-1.2.0.tgz b/pkg/repo/testdata/repository/sprocket-1.2.0.tgz index 82188a99b..6fdc73c2b 100644 Binary files a/pkg/repo/testdata/repository/sprocket-1.2.0.tgz and b/pkg/repo/testdata/repository/sprocket-1.2.0.tgz differ diff --git a/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz b/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz index 90cb34bd5..6f1e8564c 100644 Binary files a/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz and b/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz differ diff --git a/pkg/resolver/resolver.go b/pkg/resolver/resolver.go index 1242be8b8..b015db759 100644 --- a/pkg/resolver/resolver.go +++ b/pkg/resolver/resolver.go @@ -27,7 +27,6 @@ import ( "github.com/pkg/errors" "helm.sh/helm/pkg/chart" - "helm.sh/helm/pkg/helmpath" "helm.sh/helm/pkg/provenance" "helm.sh/helm/pkg/repo" ) @@ -35,34 +34,33 @@ import ( // Resolver resolves dependencies from semantic version ranges to a particular version. type Resolver struct { chartpath string - helmhome helmpath.Home + client *repo.Client } // New creates a new resolver for a given chart and a given helm home. -func New(chartpath string, helmhome helmpath.Home) *Resolver { +func New(chartpath string, client *repo.Client) *Resolver { return &Resolver{ chartpath: chartpath, - helmhome: helmhome, + client: client, } } // Resolve resolves dependencies and returns a lock file with the resolution. -func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string, d string) (*chart.Lock, error) { +func (r *Resolver) Resolve(reqs []*chart.Dependency, d string) (*chart.Lock, error) { // Now we clone the dependencies, locking as we go. locked := make([]*chart.Dependency, len(reqs)) missing := []string{} for i, d := range reqs { - if strings.HasPrefix(d.Repository, "file://") { + if strings.HasPrefix(d.Name, "file://") { - if _, err := GetLocalPath(d.Repository, r.chartpath); err != nil { + if _, err := GetLocalPath(d.Name, r.chartpath); err != nil { return nil, err } locked[i] = &chart.Dependency{ - Name: d.Name, - Repository: d.Repository, - Version: d.Version, + Name: d.Name, + Version: d.Version, } continue } @@ -71,25 +69,19 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name) } - repoIndex, err := repo.LoadIndexFile(r.helmhome.CacheIndex(repoNames[d.Name])) + tags, err := r.client.FetchTags(d.Name) if err != nil { - return nil, errors.Wrap(err, "no cached repo found. (try 'helm repo update')") - } - - vs, ok := repoIndex.Entries[d.Name] - if !ok { - return nil, errors.Errorf("%s chart not found in repo %s", d.Name, d.Repository) + return nil, errors.Wrapf(err, "could not fetch tags for dependency %q", d.Name) } locked[i] = &chart.Dependency{ - Name: d.Name, - Repository: d.Repository, + Name: d.Name, } found := false // The version are already sorted and hence the first one to satisfy the constraint is used - for _, ver := range vs { - v, err := semver.NewVersion(ver.Version) - if err != nil || len(ver.URLs) == 0 { + for _, ver := range tags { + v, err := semver.NewVersion(ver) + if err != nil { // Not a legit entry. continue } diff --git a/pkg/resolver/resolver_test.go b/pkg/resolver/resolver_test.go index c52088fc8..d0434cef2 100644 --- a/pkg/resolver/resolver_test.go +++ b/pkg/resolver/resolver_test.go @@ -16,12 +16,53 @@ limitations under the License. package resolver import ( + "io/ioutil" + "path" "testing" "helm.sh/helm/pkg/chart" + "helm.sh/helm/pkg/repo" + "helm.sh/helm/pkg/repo/repotest" ) func TestResolve(t *testing.T) { + cache, err := ioutil.TempDir("", "helm-resolver-test") + if err != nil { + t.Fatal(err) + } + + registryClient := repo.NewClient(&repo.ClientOptions{ + Out: ioutil.Discard, + CacheRootDir: cache, + }) + + testRepo := repotest.NewServer() + registryURL := testRepo.URL() + + versions := []string{"0.1.0", "0.2.0", "0.3.0"} + + for _, ver := range versions { + ch := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "alpine", + Version: ver, + }, + } + + ref, err := repo.ParseNameTag(path.Join(registryURL, ch.Metadata.Name), ch.Metadata.Version) + if err != nil { + t.Fatal(err) + } + + if err := registryClient.SaveChart(ch, registryURL); err != nil { + t.Fatal(err) + } + + if err := registryClient.PushChart(ref); err != nil { + t.Fatal(err) + } + } + tests := []struct { name string req []*chart.Dependency @@ -31,110 +72,120 @@ func TestResolve(t *testing.T) { { name: "version failure", req: []*chart.Dependency{ - {Name: "oedipus-rex", Repository: "http://example.com", Version: ">a1"}, + {Name: path.Join(registryURL, "oedipus-rex"), Version: ">a1"}, }, err: true, }, { name: "cache index failure", req: []*chart.Dependency{ - {Name: "oedipus-rex", Repository: "http://example.com", Version: "1.0.0"}, + {Name: path.Join(registryURL, "oedipus-rex"), Version: "1.0.0"}, }, err: true, }, { name: "chart not found failure", req: []*chart.Dependency{ - {Name: "redis", Repository: "http://example.com", Version: "1.0.0"}, + {Name: path.Join(registryURL, "redis"), Version: "1.0.0"}, }, err: true, }, { name: "constraint not satisfied failure", req: []*chart.Dependency{ - {Name: "alpine", Repository: "http://example.com", Version: ">=1.0.0"}, + {Name: path.Join(registryURL, "alpine"), Version: ">=1.0.0"}, }, err: true, }, { name: "valid lock", req: []*chart.Dependency{ - {Name: "alpine", Repository: "http://example.com", Version: ">=0.1.0"}, + {Name: path.Join(registryURL, "alpine"), Version: ">=0.1.0"}, + }, + expect: &chart.Lock{ + Dependencies: []*chart.Dependency{ + {Name: path.Join(registryURL, "alpine"), Version: "0.3.0"}, + }, + }, + }, + { + name: "exact lock", + req: []*chart.Dependency{ + {Name: path.Join(registryURL, "alpine"), Version: "=0.1.0"}, }, expect: &chart.Lock{ Dependencies: []*chart.Dependency{ - {Name: "alpine", Repository: "http://example.com", Version: "0.2.0"}, + {Name: path.Join(registryURL, "alpine"), Version: "0.1.0"}, }, }, }, { name: "repo from valid local path", req: []*chart.Dependency{ - {Name: "signtest", Repository: "file://../../../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"}, + {Name: "file://../../../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"}, }, expect: &chart.Lock{ Dependencies: []*chart.Dependency{ - {Name: "signtest", Repository: "file://../../../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"}, + {Name: "file://../../../../cmd/helm/testdata/testcharts/signtest", Version: "0.1.0"}, }, }, }, { name: "repo from invalid local path", req: []*chart.Dependency{ - {Name: "notexist", Repository: "file://../testdata/notexist", Version: "0.1.0"}, + {Name: "file://../testdata/notexist", Version: "0.1.0"}, }, err: true, }, } - repoNames := map[string]string{"alpine": "kubernetes-charts", "redis": "kubernetes-charts"} - r := New("testdata/chartpath", "testdata/helmhome") + r := New("testdata/chartpath", registryClient) for _, tt := range tests { - hash, err := HashReq(tt.req) - if err != nil { - t.Fatal(err) - } + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + hash, err := HashReq(tt.req) + if err != nil { + t.Fatal(err) + } - l, err := r.Resolve(tt.req, repoNames, hash) - if err != nil { - if tt.err { - continue + l, err := r.Resolve(tt.req, hash) + if err != nil { + if tt.err { + return + } + t.Fatal(err) } - t.Fatal(err) - } - if tt.err { - t.Fatalf("Expected error in test %q", tt.name) - } + if tt.err { + t.Fatalf("Expected error") + } - if h, err := HashReq(tt.req); err != nil { - t.Fatal(err) - } else if h != l.Digest { - t.Errorf("%q: hashes don't match.", tt.name) - } + if h, err := HashReq(tt.req); err != nil { + t.Fatal(err) + } else if h != l.Digest { + t.Errorf("hashes don't match. expected '%s', got '%s'", h, l.Digest) + } - // Check fields. - if len(l.Dependencies) != len(tt.req) { - t.Errorf("%s: wrong number of dependencies in lock", tt.name) - } - d0 := l.Dependencies[0] - e0 := tt.expect.Dependencies[0] - if d0.Name != e0.Name { - t.Errorf("%s: expected name %s, got %s", tt.name, e0.Name, d0.Name) - } - if d0.Repository != e0.Repository { - t.Errorf("%s: expected repo %s, got %s", tt.name, e0.Repository, d0.Repository) - } - if d0.Version != e0.Version { - t.Errorf("%s: expected version %s, got %s", tt.name, e0.Version, d0.Version) - } + // Check fields. + if len(l.Dependencies) != len(tt.req) { + t.Errorf("wrong number of dependencies in lock. expected %d, got %d", len(tt.req), len(l.Dependencies)) + } + d0 := l.Dependencies[0] + e0 := tt.expect.Dependencies[0] + if d0.Name != e0.Name { + t.Errorf("expected name %s, got %s", e0.Name, d0.Name) + } + if d0.Version != e0.Version { + t.Errorf("expected version %s, got %s", e0.Version, d0.Version) + } + }) } } func TestHashReq(t *testing.T) { - expect := "sha256:d661820b01ed7bcf26eed8f01cf16380e0a76326ba33058d3150f919d9b15bc0" + expect := "sha256:3aa1f5e784c4609f4db27c175e081e5ffab60ea4a27f87d889bb1ed273e49f75" req := []*chart.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "http://localhost:8879/charts"}, + {Name: "alpine", Version: "0.1.0"}, } h, err := HashReq(req) if err != nil {