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>
pull/11771/head
Christoph Obexer 3 years ago
parent ff61915cda
commit 3976c0621a

@ -77,6 +77,8 @@ type EnvSettings struct {
Debug bool Debug bool
// RegistryConfig is the path to the registry config file. // RegistryConfig is the path to the registry config file.
RegistryConfig string RegistryConfig string
// RegistryAliasConfig is the path to the registry alias config file.
RegistryAliasConfig string
// RepositoryConfig is the path to the repositories file. // RepositoryConfig is the path to the repositories file.
RepositoryConfig string RepositoryConfig string
// RepositoryCache is the path to the repository cache directory. // RepositoryCache is the path to the repository cache directory.
@ -109,6 +111,7 @@ func New() *EnvSettings {
KubeInsecureSkipTLSVerify: envBoolOr("HELM_KUBEINSECURE_SKIP_TLS_VERIFY", false), KubeInsecureSkipTLSVerify: envBoolOr("HELM_KUBEINSECURE_SKIP_TLS_VERIFY", false),
PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")),
RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry/config.json")), 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")), RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")),
RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")),
ContentCache: envOr("HELM_CONTENT_CACHE", helmpath.CachePath("content")), 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.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.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.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.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.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)") 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 { func (s *EnvSettings) EnvVars() map[string]string {
envvars := map[string]string{ envvars := map[string]string{
"HELM_BIN": os.Args[0], "HELM_BIN": os.Args[0],
"HELM_CACHE_HOME": helmpath.CachePath(""), "HELM_CACHE_HOME": helmpath.CachePath(""),
"HELM_CONFIG_HOME": helmpath.ConfigPath(""), "HELM_CONFIG_HOME": helmpath.ConfigPath(""),
"HELM_DATA_HOME": helmpath.DataPath(""), "HELM_DATA_HOME": helmpath.DataPath(""),
"HELM_DEBUG": fmt.Sprint(s.Debug), "HELM_DEBUG": fmt.Sprint(s.Debug),
"HELM_PLUGINS": s.PluginsDirectory, "HELM_PLUGINS": s.PluginsDirectory,
"HELM_REGISTRY_CONFIG": s.RegistryConfig, "HELM_REGISTRY_CONFIG": s.RegistryConfig,
"HELM_REPOSITORY_CACHE": s.RepositoryCache, "HELM_REGISTRY_ALIAS_CONFIG": s.RegistryAliasConfig,
"HELM_CONTENT_CACHE": s.ContentCache, "HELM_REPOSITORY_CACHE": s.RepositoryCache,
"HELM_REPOSITORY_CONFIG": s.RepositoryConfig, "HELM_CONTENT_CACHE": s.ContentCache,
"HELM_NAMESPACE": s.Namespace(), "HELM_REPOSITORY_CONFIG": s.RepositoryConfig,
"HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory), "HELM_NAMESPACE": s.Namespace(),
"HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit), "HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory),
"HELM_QPS": strconv.FormatFloat(float64(s.QPS), 'f', 2, 32), "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. // broken, these are populated from helm flags and not kubeconfig.
"HELM_KUBECONTEXT": s.KubeContext, "HELM_KUBECONTEXT": s.KubeContext,

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

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

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

@ -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{ man := &downloader.Manager{
Out: out, Out: out,
ChartPath: chartpath, ChartPath: chartpath,
Keyring: client.Keyring, Keyring: client.Keyring,
SkipUpdate: client.SkipRefresh, SkipUpdate: client.SkipRefresh,
Getters: getter.All(settings), Getters: getter.All(settings),
RegistryClient: registryClient, RegistryClient: registryClient,
RepositoryConfig: settings.RepositoryConfig, RegistryAliasConfig: settings.RegistryAliasConfig,
RepositoryCache: settings.RepositoryCache, RepositoryConfig: settings.RepositoryConfig,
ContentCache: settings.ContentCache, RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug, ContentCache: settings.ContentCache,
Debug: settings.Debug,
} }
if client.Verify { if client.Verify {
man.Verify = downloader.VerifyIfPossible man.Verify = downloader.VerifyIfPossible

@ -65,16 +65,17 @@ func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Comma
} }
man := &downloader.Manager{ man := &downloader.Manager{
Out: out, Out: out,
ChartPath: chartpath, ChartPath: chartpath,
Keyring: client.Keyring, Keyring: client.Keyring,
SkipUpdate: client.SkipRefresh, SkipUpdate: client.SkipRefresh,
Getters: getter.All(settings), Getters: getter.All(settings),
RegistryClient: registryClient, RegistryClient: registryClient,
RepositoryConfig: settings.RepositoryConfig, RegistryAliasConfig: settings.RegistryAliasConfig,
RepositoryCache: settings.RepositoryCache, RepositoryConfig: settings.RepositoryConfig,
ContentCache: settings.ContentCache, RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug, ContentCache: settings.ContentCache,
Debug: settings.Debug,
} }
if client.Verify { if client.Verify {
man.Verify = downloader.VerifyAlways 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 err := action.CheckDependencies(chartRequested, req); err != nil {
if client.DependencyUpdate { if client.DependencyUpdate {
man := &downloader.Manager{ man := &downloader.Manager{
Out: out, Out: out,
ChartPath: cp, ChartPath: cp,
Keyring: client.Keyring, Keyring: client.Keyring,
SkipUpdate: false, SkipUpdate: false,
Getters: p, Getters: p,
RepositoryConfig: settings.RepositoryConfig, RegistryAliasConfig: settings.RegistryAliasConfig,
RepositoryCache: settings.RepositoryCache, RepositoryConfig: settings.RepositoryConfig,
ContentCache: settings.ContentCache, RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug, ContentCache: settings.ContentCache,
RegistryClient: client.GetRegistryClient(), Debug: settings.Debug,
RegistryClient: client.GetRegistryClient(),
} }
if err := man.Update(); err != nil { if err := man.Update(); err != nil {
return nil, err return nil, err

@ -92,15 +92,16 @@ func newPackageCmd(out io.Writer) *cobra.Command {
if client.DependencyUpdate { if client.DependencyUpdate {
downloadManager := &downloader.Manager{ downloadManager := &downloader.Manager{
Out: io.Discard, Out: io.Discard,
ChartPath: path, ChartPath: path,
Keyring: client.Keyring, Keyring: client.Keyring,
Getters: p, Getters: p,
Debug: settings.Debug, Debug: settings.Debug,
RegistryClient: registryClient, RegistryClient: registryClient,
RepositoryConfig: settings.RepositoryConfig, RegistryAliasConfig: settings.RegistryAliasConfig,
RepositoryCache: settings.RepositoryCache, RepositoryConfig: settings.RepositoryConfig,
ContentCache: settings.ContentCache, RepositoryCache: settings.RepositoryCache,
ContentCache: settings.ContentCache,
} }
if err := downloadManager.Update(); err != nil { if err := downloadManager.Update(); err != nil {

@ -287,6 +287,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg
) )
cmd.AddCommand( cmd.AddCommand(
newAliasCmd(actionConfig, out),
newRegistryCmd(actionConfig, out), newRegistryCmd(actionConfig, out),
newPushCmd(actionConfig, out), newPushCmd(actionConfig, out),
) )

@ -17,6 +17,7 @@ HELM_MAX_HISTORY
HELM_NAMESPACE HELM_NAMESPACE
HELM_PLUGINS HELM_PLUGINS
HELM_QPS HELM_QPS
HELM_REGISTRY_ALIAS_CONFIG
HELM_REGISTRY_CONFIG HELM_REGISTRY_CONFIG
HELM_REPOSITORY_CACHE HELM_REPOSITORY_CACHE
HELM_REPOSITORY_CONFIG 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) 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 { if client.DependencyUpdate {
man := &downloader.Manager{ man := &downloader.Manager{
Out: out, Out: out,
ChartPath: chartPath, ChartPath: chartPath,
Keyring: client.Keyring, Keyring: client.Keyring,
SkipUpdate: false, SkipUpdate: false,
Getters: p, Getters: p,
RepositoryConfig: settings.RepositoryConfig, RegistryAliasConfig: settings.RegistryAliasConfig,
RepositoryCache: settings.RepositoryCache, RepositoryConfig: settings.RepositoryConfig,
ContentCache: settings.ContentCache, RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug, ContentCache: settings.ContentCache,
Debug: settings.Debug,
} }
if err := man.Update(); err != nil { if err := man.Update(); err != nil {
return err return err

@ -71,10 +71,11 @@ type Manager struct {
// SkipUpdate indicates that the repository should not be updated first. // SkipUpdate indicates that the repository should not be updated first.
SkipUpdate bool SkipUpdate bool
// Getter collection for the operation // Getter collection for the operation
Getters []getter.Provider Getters []getter.Provider
RegistryClient *registry.Client RegistryClient *registry.Client
RepositoryConfig string RegistryAliasConfig string
RepositoryCache string RepositoryConfig string
RepositoryCache string
// ContentCache is a location where a cache of charts can be stored // ContentCache is a location where a cache of charts can be stored
ContentCache string ContentCache string
@ -564,11 +565,20 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
rf, err := loadRepoConfig(m.RepositoryConfig) rf, err := loadRepoConfig(m.RepositoryConfig)
if err != nil { if err != nil {
if errors.Is(err, stdfs.ErrNotExist) { 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) reposMap := make(map[string]string)
@ -593,6 +603,8 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
continue continue
} }
dd.Repository = aliases.Expand(dd.Repository)
if registry.IsOCI(dd.Repository) { if registry.IsOCI(dd.Repository) {
reposMap[dd.Name] = dd.Repository reposMap[dd.Name] = dd.Repository
continue continue
@ -600,7 +612,7 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
found := false found := false
for _, repo := range repos { for _, repo := range rf.Repositories {
if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) || if (strings.HasPrefix(dd.Repository, "@") && strings.TrimPrefix(dd.Repository, "@") == repo.Name) ||
(strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) { (strings.HasPrefix(dd.Repository, "alias:") && strings.TrimPrefix(dd.Repository, "alias:") == repo.Name) {
found = true found = true

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

@ -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 is the legacy reserved media type for Helm chart package content.
LegacyChartLayerMediaType = "application/tar+gzip" LegacyChartLayerMediaType = "application/tar+gzip"
// APIVersionV1 is the v1 API version for the aliases and substitutions file.
APIVersionV1 = "v1"
) )

@ -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…
Cancel
Save