mirror of
https://github.com/helm/helm.git
synced 2026-02-20 00:13:02 -05:00
add helm alias subcommand
helm alias set: configure an alias that maps a name to an oci url used like with legacy repositories `@name` or `alias:name` helm alias list: shows aliases and substitutions helm alias substitute: configure registry url substitutions for example substitute oci://some-vendor.example.com/vendor/charts with oci://internal.example.com/charts/3rdparty/vendor and thus helm will never contact some-vendor.example.com but instead resolve the vendors charts using internal.example.com Signed-off-by: Christoph Obexer <cobexer@gmail.com>
This commit is contained in:
parent
ff61915cda
commit
3976c0621a
17 changed files with 705 additions and 70 deletions
|
|
@ -77,6 +77,8 @@ type EnvSettings struct {
|
|||
Debug bool
|
||||
// RegistryConfig is the path to the registry config file.
|
||||
RegistryConfig string
|
||||
// RegistryAliasConfig is the path to the registry alias config file.
|
||||
RegistryAliasConfig string
|
||||
// RepositoryConfig is the path to the repositories file.
|
||||
RepositoryConfig string
|
||||
// RepositoryCache is the path to the repository cache directory.
|
||||
|
|
@ -109,6 +111,7 @@ func New() *EnvSettings {
|
|||
KubeInsecureSkipTLSVerify: envBoolOr("HELM_KUBEINSECURE_SKIP_TLS_VERIFY", false),
|
||||
PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")),
|
||||
RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry/config.json")),
|
||||
RegistryAliasConfig: envOr("HELM_REGISTRY_ALIAS_CONFIG", helmpath.ConfigPath("registry/aliases.yaml")),
|
||||
RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")),
|
||||
RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")),
|
||||
ContentCache: envOr("HELM_CONTENT_CACHE", helmpath.CachePath("content")),
|
||||
|
|
@ -162,6 +165,7 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) {
|
|||
fs.BoolVar(&s.KubeInsecureSkipTLSVerify, "kube-insecure-skip-tls-verify", s.KubeInsecureSkipTLSVerify, "if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure")
|
||||
fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output")
|
||||
fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file")
|
||||
fs.StringVar(&s.RegistryAliasConfig, "registry-alias-config", s.RegistryAliasConfig, "path to the registry alias config file")
|
||||
fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs")
|
||||
fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the directory containing cached repository indexes")
|
||||
fs.StringVar(&s.ContentCache, "content-cache", s.ContentCache, "path to the directory containing cached content (e.g. charts)")
|
||||
|
|
@ -241,20 +245,21 @@ func envColorMode() string {
|
|||
|
||||
func (s *EnvSettings) EnvVars() map[string]string {
|
||||
envvars := map[string]string{
|
||||
"HELM_BIN": os.Args[0],
|
||||
"HELM_CACHE_HOME": helmpath.CachePath(""),
|
||||
"HELM_CONFIG_HOME": helmpath.ConfigPath(""),
|
||||
"HELM_DATA_HOME": helmpath.DataPath(""),
|
||||
"HELM_DEBUG": fmt.Sprint(s.Debug),
|
||||
"HELM_PLUGINS": s.PluginsDirectory,
|
||||
"HELM_REGISTRY_CONFIG": s.RegistryConfig,
|
||||
"HELM_REPOSITORY_CACHE": s.RepositoryCache,
|
||||
"HELM_CONTENT_CACHE": s.ContentCache,
|
||||
"HELM_REPOSITORY_CONFIG": s.RepositoryConfig,
|
||||
"HELM_NAMESPACE": s.Namespace(),
|
||||
"HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory),
|
||||
"HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit),
|
||||
"HELM_QPS": strconv.FormatFloat(float64(s.QPS), 'f', 2, 32),
|
||||
"HELM_BIN": os.Args[0],
|
||||
"HELM_CACHE_HOME": helmpath.CachePath(""),
|
||||
"HELM_CONFIG_HOME": helmpath.ConfigPath(""),
|
||||
"HELM_DATA_HOME": helmpath.DataPath(""),
|
||||
"HELM_DEBUG": fmt.Sprint(s.Debug),
|
||||
"HELM_PLUGINS": s.PluginsDirectory,
|
||||
"HELM_REGISTRY_CONFIG": s.RegistryConfig,
|
||||
"HELM_REGISTRY_ALIAS_CONFIG": s.RegistryAliasConfig,
|
||||
"HELM_REPOSITORY_CACHE": s.RepositoryCache,
|
||||
"HELM_CONTENT_CACHE": s.ContentCache,
|
||||
"HELM_REPOSITORY_CONFIG": s.RepositoryConfig,
|
||||
"HELM_NAMESPACE": s.Namespace(),
|
||||
"HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory),
|
||||
"HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit),
|
||||
"HELM_QPS": strconv.FormatFloat(float64(s.QPS), 'f', 2, 32),
|
||||
|
||||
// broken, these are populated from helm flags and not kubeconfig.
|
||||
"HELM_KUBECONTEXT": s.KubeContext,
|
||||
|
|
|
|||
42
pkg/cmd/alias.go
Normal file
42
pkg/cmd/alias.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/v4/pkg/action"
|
||||
)
|
||||
|
||||
const aliasHelp = `
|
||||
This command consists of multiple subcommands to interact with OCI aliases.
|
||||
`
|
||||
|
||||
func newAliasCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "alias",
|
||||
Short: "manage OCI aliases and substitutions",
|
||||
Long: aliasHelp,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newAliasListCmd(cfg, out),
|
||||
newAliasSetCmd(cfg, out),
|
||||
newAliasSubstituteCmd(cfg, out),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
75
pkg/cmd/alias_list.go
Normal file
75
pkg/cmd/alias_list.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/gosuri/uitable"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/v4/pkg/action"
|
||||
"helm.sh/helm/v4/pkg/cli/output"
|
||||
"helm.sh/helm/v4/pkg/cmd/require"
|
||||
"helm.sh/helm/v4/pkg/registry"
|
||||
)
|
||||
|
||||
const aliasListDesc = `
|
||||
List registry aliases and substitutions.
|
||||
`
|
||||
|
||||
func newAliasListCmd(_ *action.Configuration, out io.Writer) *cobra.Command {
|
||||
var aliasesOpt, substitutionsOpt bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list aliases and substitutions",
|
||||
Long: aliasListDesc,
|
||||
Args: require.NoArgs,
|
||||
ValidArgsFunction: noMoreArgsCompFunc,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
a, _ := registry.LoadAliasesFile(settings.RegistryAliasConfig)
|
||||
|
||||
if aliasesOpt || !substitutionsOpt {
|
||||
table := uitable.New()
|
||||
table.AddRow("ALIAS", "URL")
|
||||
for a, url := range a.Aliases {
|
||||
table.AddRow(a, url)
|
||||
}
|
||||
err = output.EncodeTable(out, table)
|
||||
}
|
||||
|
||||
if substitutionsOpt || !aliasesOpt {
|
||||
table := uitable.New()
|
||||
table.AddRow("SUBSTITUTION", "REPLACEMENT")
|
||||
for s, r := range a.Substitutions {
|
||||
table.AddRow(s, r)
|
||||
}
|
||||
err = output.EncodeTable(out, table)
|
||||
}
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.BoolVarP(&aliasesOpt, "aliases", "a", false, "list aliases")
|
||||
f.BoolVarP(&substitutionsOpt, "substitutions", "s", false, "list substitutions")
|
||||
|
||||
return cmd
|
||||
}
|
||||
79
pkg/cmd/alias_set.go
Normal file
79
pkg/cmd/alias_set.go
Normal file
|
|
@ -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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/v4/pkg/action"
|
||||
"helm.sh/helm/v4/pkg/cmd/require"
|
||||
"helm.sh/helm/v4/pkg/registry"
|
||||
)
|
||||
|
||||
const aliasSetDesc = `
|
||||
Set or remove an alias for an OCI registry.
|
||||
`
|
||||
|
||||
func newAliasSetCmd(_ *action.Configuration, _ io.Writer) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "set NAME [URL]",
|
||||
Short: "configure the named alias",
|
||||
Long: aliasSetDesc,
|
||||
Args: require.MinimumNArgs(1),
|
||||
ValidArgsFunction: noMoreArgsCompFunc,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
alias := args[0]
|
||||
var value *string
|
||||
if len(args) > 1 {
|
||||
value = &args[1]
|
||||
}
|
||||
|
||||
err := setAlias(settings.RegistryAliasConfig, alias, value)
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func setAlias(aliasesFile, alias string, value *string) error {
|
||||
if strings.Contains(alias, "/") {
|
||||
return fmt.Errorf("alias name (%s) contains '/', please specify a different name without '/'", alias)
|
||||
}
|
||||
|
||||
a, err := registry.LoadAliasesFile(aliasesFile)
|
||||
if err != nil && !isNotExist(err) {
|
||||
return fmt.Errorf("failed to load aliases: %w", err)
|
||||
}
|
||||
|
||||
if value != nil {
|
||||
a.SetAlias(alias, *value)
|
||||
} else {
|
||||
a.RemoveAlias(alias)
|
||||
}
|
||||
|
||||
if err := a.WriteAliasesFile(aliasesFile, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
74
pkg/cmd/alias_substitute.go
Normal file
74
pkg/cmd/alias_substitute.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
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 cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/v4/pkg/action"
|
||||
"helm.sh/helm/v4/pkg/cmd/require"
|
||||
"helm.sh/helm/v4/pkg/registry"
|
||||
)
|
||||
|
||||
const aliasSubstituteDesc = `
|
||||
Set or remove a registry substitution.
|
||||
`
|
||||
|
||||
func newAliasSubstituteCmd(_ *action.Configuration, _ io.Writer) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "substitute URL [URL]",
|
||||
Short: "configure a OCI registry URL substitution",
|
||||
Long: aliasSubstituteDesc,
|
||||
Args: require.MinimumNArgs(1),
|
||||
ValidArgsFunction: noMoreArgsCompFunc,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
substitution := args[0]
|
||||
var replacement *string
|
||||
if len(args) > 1 {
|
||||
replacement = &args[1]
|
||||
}
|
||||
|
||||
err := setSubstitution(settings.RegistryAliasConfig, substitution, replacement)
|
||||
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func setSubstitution(aliasesFile, substitution string, replacement *string) error {
|
||||
a, err := registry.LoadAliasesFile(aliasesFile)
|
||||
if err != nil && !isNotExist(err) {
|
||||
return fmt.Errorf("failed to load aliases: %w", err)
|
||||
}
|
||||
|
||||
if replacement != nil {
|
||||
a.SetSubstitution(substitution, *replacement)
|
||||
} else {
|
||||
a.RemoveSubstitution(substitution)
|
||||
}
|
||||
|
||||
if err := a.WriteAliasesFile(aliasesFile, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -61,16 +61,17 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
|
|||
}
|
||||
|
||||
man := &downloader.Manager{
|
||||
Out: out,
|
||||
ChartPath: chartpath,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: client.SkipRefresh,
|
||||
Getters: getter.All(settings),
|
||||
RegistryClient: registryClient,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Debug: settings.Debug,
|
||||
Out: out,
|
||||
ChartPath: chartpath,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: client.SkipRefresh,
|
||||
Getters: getter.All(settings),
|
||||
RegistryClient: registryClient,
|
||||
RegistryAliasConfig: settings.RegistryAliasConfig,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Debug: settings.Debug,
|
||||
}
|
||||
if client.Verify {
|
||||
man.Verify = downloader.VerifyIfPossible
|
||||
|
|
|
|||
|
|
@ -65,16 +65,17 @@ func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Comma
|
|||
}
|
||||
|
||||
man := &downloader.Manager{
|
||||
Out: out,
|
||||
ChartPath: chartpath,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: client.SkipRefresh,
|
||||
Getters: getter.All(settings),
|
||||
RegistryClient: registryClient,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Debug: settings.Debug,
|
||||
Out: out,
|
||||
ChartPath: chartpath,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: client.SkipRefresh,
|
||||
Getters: getter.All(settings),
|
||||
RegistryClient: registryClient,
|
||||
RegistryAliasConfig: settings.RegistryAliasConfig,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Debug: settings.Debug,
|
||||
}
|
||||
if client.Verify {
|
||||
man.Verify = downloader.VerifyAlways
|
||||
|
|
|
|||
|
|
@ -285,16 +285,17 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
|
|||
if err := action.CheckDependencies(chartRequested, req); err != nil {
|
||||
if client.DependencyUpdate {
|
||||
man := &downloader.Manager{
|
||||
Out: out,
|
||||
ChartPath: cp,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: false,
|
||||
Getters: p,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Debug: settings.Debug,
|
||||
RegistryClient: client.GetRegistryClient(),
|
||||
Out: out,
|
||||
ChartPath: cp,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: false,
|
||||
Getters: p,
|
||||
RegistryAliasConfig: settings.RegistryAliasConfig,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Debug: settings.Debug,
|
||||
RegistryClient: client.GetRegistryClient(),
|
||||
}
|
||||
if err := man.Update(); err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -92,15 +92,16 @@ func newPackageCmd(out io.Writer) *cobra.Command {
|
|||
|
||||
if client.DependencyUpdate {
|
||||
downloadManager := &downloader.Manager{
|
||||
Out: io.Discard,
|
||||
ChartPath: path,
|
||||
Keyring: client.Keyring,
|
||||
Getters: p,
|
||||
Debug: settings.Debug,
|
||||
RegistryClient: registryClient,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Out: io.Discard,
|
||||
ChartPath: path,
|
||||
Keyring: client.Keyring,
|
||||
Getters: p,
|
||||
Debug: settings.Debug,
|
||||
RegistryClient: registryClient,
|
||||
RegistryAliasConfig: settings.RegistryAliasConfig,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
}
|
||||
|
||||
if err := downloadManager.Update(); err != nil {
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
|
|||
)
|
||||
|
||||
cmd.AddCommand(
|
||||
newAliasCmd(actionConfig, out),
|
||||
newRegistryCmd(actionConfig, out),
|
||||
newPushCmd(actionConfig, out),
|
||||
)
|
||||
|
|
|
|||
1
pkg/cmd/testdata/output/env-comp.txt
vendored
1
pkg/cmd/testdata/output/env-comp.txt
vendored
|
|
@ -17,6 +17,7 @@ HELM_MAX_HISTORY
|
|||
HELM_NAMESPACE
|
||||
HELM_PLUGINS
|
||||
HELM_QPS
|
||||
HELM_REGISTRY_ALIAS_CONFIG
|
||||
HELM_REGISTRY_CONFIG
|
||||
HELM_REPOSITORY_CACHE
|
||||
HELM_REPOSITORY_CONFIG
|
||||
|
|
|
|||
|
|
@ -203,15 +203,16 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
|
|||
err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err)
|
||||
if client.DependencyUpdate {
|
||||
man := &downloader.Manager{
|
||||
Out: out,
|
||||
ChartPath: chartPath,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: false,
|
||||
Getters: p,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Debug: settings.Debug,
|
||||
Out: out,
|
||||
ChartPath: chartPath,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: false,
|
||||
Getters: p,
|
||||
RegistryAliasConfig: settings.RegistryAliasConfig,
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
RepositoryCache: settings.RepositoryCache,
|
||||
ContentCache: settings.ContentCache,
|
||||
Debug: settings.Debug,
|
||||
}
|
||||
if err := man.Update(); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -71,10 +71,11 @@ type Manager struct {
|
|||
// SkipUpdate indicates that the repository should not be updated first.
|
||||
SkipUpdate bool
|
||||
// Getter collection for the operation
|
||||
Getters []getter.Provider
|
||||
RegistryClient *registry.Client
|
||||
RepositoryConfig string
|
||||
RepositoryCache string
|
||||
Getters []getter.Provider
|
||||
RegistryClient *registry.Client
|
||||
RegistryAliasConfig string
|
||||
RepositoryConfig string
|
||||
RepositoryCache string
|
||||
|
||||
// ContentCache is a location where a cache of charts can be stored
|
||||
ContentCache string
|
||||
|
|
@ -564,11 +565,20 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
|
|||
rf, err := loadRepoConfig(m.RepositoryConfig)
|
||||
if err != nil {
|
||||
if errors.Is(err, stdfs.ErrNotExist) {
|
||||
return make(map[string]string), nil
|
||||
rf = repo.NewFile()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
aliases, err := registry.LoadAliasesFile(m.RegistryAliasConfig)
|
||||
if err != nil {
|
||||
if errors.Is(err, stdfs.ErrNotExist) {
|
||||
aliases = registry.NewAliasesFile()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
repos := rf.Repositories
|
||||
|
||||
reposMap := make(map[string]string)
|
||||
|
||||
|
|
@ -593,6 +603,8 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
|
|||
continue
|
||||
}
|
||||
|
||||
dd.Repository = aliases.Expand(dd.Repository)
|
||||
|
||||
if registry.IsOCI(dd.Repository) {
|
||||
reposMap[dd.Name] = dd.Repository
|
||||
continue
|
||||
|
|
@ -600,7 +612,7 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
|
|||
|
||||
found := false
|
||||
|
||||
for _, repo := range repos {
|
||||
for _, repo := range rf.Repositories {
|
||||
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
|
||||
|
|
|
|||
153
pkg/registry/alias.go
Normal file
153
pkg/registry/alias.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
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 (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// Aliases represents the registry/aliases.yaml file
|
||||
type Aliases struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
Aliases map[string]string `json:"aliases"`
|
||||
Substitutions map[string]string `json:"substitutions"`
|
||||
}
|
||||
|
||||
// NewAliasesFile generates an empty aliases file.
|
||||
//
|
||||
// APIVersion is automatically set.
|
||||
func NewAliasesFile() *Aliases {
|
||||
return &Aliases{
|
||||
APIVersion: APIVersionV1,
|
||||
Aliases: map[string]string{},
|
||||
Substitutions: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAliasesFile takes a file at the given path and returns an Aliases object
|
||||
func LoadAliasesFile(path string) (*Aliases, error) {
|
||||
a := NewAliasesFile()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return a, fmt.Errorf("couldn't load aliases file (%s): %w", path, err)
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(b, a)
|
||||
return a, err
|
||||
}
|
||||
|
||||
// SetAlias adds or updates an alias.
|
||||
func (a *Aliases) SetAlias(alias, url string) {
|
||||
a.Aliases[alias] = url
|
||||
}
|
||||
|
||||
// RemoveAlias removes the entry from the list of repository aliases.
|
||||
// RemoveAlias returns true if the alias existed before it was deleted.
|
||||
func (a *Aliases) RemoveAlias(alias string) bool {
|
||||
_, existing := a.Aliases[alias]
|
||||
delete(a.Aliases, alias)
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
// SetSubstitution adds or updates a substitution.
|
||||
func (a *Aliases) SetSubstitution(substitution, replacement string) {
|
||||
a.Substitutions[substitution] = replacement
|
||||
}
|
||||
|
||||
// RemoveSubstitution removes the substitution and returns true if the
|
||||
// substitution existed before it was deleted.
|
||||
func (a *Aliases) RemoveSubstitution(substitution string) bool {
|
||||
_, existing := a.Substitutions[substitution]
|
||||
delete(a.Substitutions, substitution)
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
// Expand first expands aliases to their mapped value end then performs
|
||||
// prefix substitutions until no substitution matches or each substitution
|
||||
// was used at most once.
|
||||
func (a *Aliases) Expand(source string) string {
|
||||
return a.performSubstitutions(a.expandAlias(source))
|
||||
}
|
||||
|
||||
func (a *Aliases) expandAlias(source string) string {
|
||||
isAtAlias := strings.HasPrefix(source, "@")
|
||||
isLongAlias := strings.HasPrefix(source, "alias:")
|
||||
if isAtAlias || isLongAlias {
|
||||
var alias string
|
||||
if isAtAlias {
|
||||
alias = strings.TrimPrefix(source, "@")
|
||||
} else if isLongAlias {
|
||||
alias = strings.TrimPrefix(source, "alias:")
|
||||
}
|
||||
if v, existing := a.Aliases[alias]; existing {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
func (a *Aliases) performSubstitutions(source string) string {
|
||||
current := source
|
||||
|
||||
// no recursions
|
||||
used := make(map[string]bool, len(a.Substitutions))
|
||||
orderedSubstitutions := make([]string, 0, len(a.Substitutions))
|
||||
for k := range a.Substitutions {
|
||||
orderedSubstitutions = append(orderedSubstitutions, k)
|
||||
}
|
||||
sort.SliceStable(orderedSubstitutions, func(i, j int) bool {
|
||||
return len(orderedSubstitutions[i]) < len(orderedSubstitutions[j])
|
||||
})
|
||||
var changed bool
|
||||
for {
|
||||
changed = false
|
||||
for i := range orderedSubstitutions {
|
||||
k := orderedSubstitutions[i]
|
||||
if !used[k] && strings.HasPrefix(current, k) {
|
||||
used[k] = true
|
||||
current = a.Substitutions[k] + strings.TrimPrefix(current, k)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// WriteAliasesFile writes an aliases file to the given path.
|
||||
func (a *Aliases) WriteAliasesFile(path string, perm os.FileMode) error {
|
||||
data, err := yaml.Marshal(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, perm)
|
||||
}
|
||||
176
pkg/registry/alias_test.go
Normal file
176
pkg/registry/alias_test.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
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 (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testAliasesFile = "testdata/aliases.yaml"
|
||||
|
||||
func TestAliasesFile(t *testing.T) {
|
||||
a := NewAliasesFile()
|
||||
a.SetAlias("staging", "oci://example.com/charts/staging")
|
||||
a.SetAlias("production", "oci://example.com/charts/production")
|
||||
|
||||
a.SetSubstitution("oci://example.com/charts/production", "oci://example.com/qa-environment/charts/production")
|
||||
|
||||
if len(a.Aliases) != 2 {
|
||||
t.Fatal("Expected 2 aliases")
|
||||
}
|
||||
|
||||
if len(a.Substitutions) != 1 {
|
||||
t.Fatal("Expected 1 substitution")
|
||||
}
|
||||
|
||||
if !a.RemoveAlias("staging") {
|
||||
t.Fatal("Expected staging alias to exist")
|
||||
}
|
||||
|
||||
if a.RemoveAlias("staging") {
|
||||
t.Fatal("Expected staging alias to not exist")
|
||||
}
|
||||
|
||||
if len(a.Aliases) != 1 {
|
||||
t.Fatal("Expected 1 alias")
|
||||
}
|
||||
|
||||
if !a.RemoveSubstitution("oci://example.com/charts/production") {
|
||||
t.Fatal("Expected 'oci://example.com/charts/production' substitution to exist")
|
||||
}
|
||||
|
||||
if a.RemoveSubstitution("oci://example.com/charts/production") {
|
||||
t.Fatal("Expected 'oci://example.com/charts/production' substitution to not exist")
|
||||
}
|
||||
|
||||
if len(a.Substitutions) != 0 {
|
||||
t.Fatal("Expected 0 substitutions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAliasesFile(t *testing.T) {
|
||||
expects := NewAliasesFile()
|
||||
expects.SetAlias("staging", "oci://example.com/charts/staging")
|
||||
expects.SetAlias("production", "oci://example.com/charts/production")
|
||||
expects.SetAlias("dev", "oci://example.com/charts/dev")
|
||||
expects.SetSubstitution("oci://example.com/charts/dev", "oci://dev.example.com/charts")
|
||||
expects.SetSubstitution("oci://example.com/charts/staging", "oci://staging.example.com/charts")
|
||||
expects.SetSubstitution("https://example.com/stable/charts", "oci://stable.example.com/charts")
|
||||
|
||||
file, err := LoadAliasesFile(testAliasesFile)
|
||||
if err != nil {
|
||||
t.Errorf("%q could not be loaded: %s", testAliasesFile, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expects.APIVersion, file.APIVersion) {
|
||||
t.Fatalf("Unexpected apiVersion: %#v", file.APIVersion)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expects.Aliases, file.Aliases) {
|
||||
t.Fatalf("Unexpected aliases: %#v", file.Aliases)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expects.Substitutions, file.Substitutions) {
|
||||
t.Fatalf("Unexpected substitutions: %#v", file.Substitutions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAliasesFile(t *testing.T) {
|
||||
expects := NewAliasesFile()
|
||||
expects.SetAlias("dev", "oci://example.com/charts/dev")
|
||||
expects.SetSubstitution("oci://example.com/charts/dev", "oci://dev.example.com/charts")
|
||||
|
||||
file, err := os.CreateTemp(t.TempDir(), "helm-aliases")
|
||||
if err != nil {
|
||||
t.Errorf("failed to create test-file (%v)", err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
if err := expects.WriteAliasesFile(file.Name(), 0o644); err != nil {
|
||||
t.Errorf("failed to write file (%v)", err)
|
||||
}
|
||||
|
||||
aliases, err := LoadAliasesFile(file.Name())
|
||||
if err != nil {
|
||||
t.Errorf("failed to load file (%v)", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expects, aliases) {
|
||||
t.Errorf("aliases inconsistent after saving and reloading:\nexpected: %#v\nactual: %#v", expects, aliases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliasNotExists(t *testing.T) {
|
||||
if _, err := LoadAliasesFile("/this/path/does/not/exist.yaml"); err == nil {
|
||||
t.Errorf("expected err to be non-nil when path does not exist")
|
||||
} else if !strings.Contains(err.Error(), "couldn't load aliases file") {
|
||||
t.Errorf("expected prompt `couldn't load aliases file`")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAliases_performSubstitutions(t *testing.T) {
|
||||
substitutions := NewAliasesFile()
|
||||
substitutions.SetSubstitution("oci://example.com/charts", "oci://example.com/charts/dev")
|
||||
substitutions.SetSubstitution("oci://length.example.com", "oci://shorter.length.example.com")
|
||||
substitutions.SetSubstitution("oci://length.example.com/charts", "oci://longer.length.example.com/charts")
|
||||
substitutions.SetSubstitution("oci://multiple.example.com", "oci://example.com/charts")
|
||||
substitutions.SetSubstitution("oci://localhost:5000/", "oci://staging.example.com/charts/")
|
||||
substitutions.SetSubstitution("https://example.com/vendor", "oci://vendor.example.com/charts/")
|
||||
substitutions.SetSubstitution("oci://one.example.com", "oci://two.example.com")
|
||||
substitutions.SetSubstitution("oci://two.example.com", "oci://one.example.com")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basicOCIReplacement",
|
||||
source: "oci://localhost:5000/myrepo",
|
||||
want: "oci://staging.example.com/charts/myrepo",
|
||||
},
|
||||
{
|
||||
name: "exacltyAsRequested",
|
||||
source: "https://example.com/vendor-dev/some-chart-repo",
|
||||
want: "oci://vendor.example.com/charts/-dev/some-chart-repo",
|
||||
},
|
||||
{
|
||||
name: "multipleReplacements",
|
||||
source: "oci://multiple.example.com/myrepo",
|
||||
want: "oci://example.com/charts/dev/myrepo",
|
||||
},
|
||||
{
|
||||
name: "norecursion",
|
||||
source: "oci://one.example.com/myrepo",
|
||||
want: "oci://one.example.com/myrepo",
|
||||
},
|
||||
{
|
||||
name: "usedOnlyOnce",
|
||||
source: "oci://example.com/charts/myrepo",
|
||||
want: "oci://example.com/charts/dev/myrepo",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := substitutions.performSubstitutions(tt.source); got != tt.want {
|
||||
t.Errorf("Aliases.performSubstitutions() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -34,4 +34,7 @@ const (
|
|||
|
||||
// LegacyChartLayerMediaType is the legacy reserved media type for Helm chart package content.
|
||||
LegacyChartLayerMediaType = "application/tar+gzip"
|
||||
|
||||
// APIVersionV1 is the v1 API version for the aliases and substitutions file.
|
||||
APIVersionV1 = "v1"
|
||||
)
|
||||
|
|
|
|||
9
pkg/registry/testdata/aliases.yaml
vendored
Normal file
9
pkg/registry/testdata/aliases.yaml
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: v1
|
||||
aliases:
|
||||
staging: oci://example.com/charts/staging
|
||||
production: oci://example.com/charts/production
|
||||
dev: oci://example.com/charts/dev
|
||||
substitutions:
|
||||
oci://example.com/charts/dev: oci://dev.example.com/charts
|
||||
oci://example.com/charts/staging: oci://staging.example.com/charts
|
||||
https://example.com/stable/charts: oci://stable.example.com/charts
|
||||
Loading…
Reference in a new issue