pin dependencies

Signed-off-by: Roland.Ma <rolandma@yunify.com>
This commit is contained in:
Roland.Ma
2021-08-16 03:29:03 +00:00
parent eae248b3c9
commit 7bb8124a61
251 changed files with 40010 additions and 716 deletions

View File

@@ -0,0 +1,420 @@
/*
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 (
"bytes"
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/engine"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
"helm.sh/helm/v3/pkg/time"
)
// Timestamper is a function capable of producing a timestamp.Timestamper.
//
// By default, this is a time.Time function from the Helm time package. This can
// be overridden for testing though, so that timestamps are predictable.
var Timestamper = time.Now
var (
// errMissingChart indicates that a chart was not provided.
errMissingChart = errors.New("no chart provided")
// errMissingRelease indicates that a release (name) was not provided.
errMissingRelease = errors.New("no release provided")
// errInvalidRevision indicates that an invalid release revision number was provided.
errInvalidRevision = errors.New("invalid release revision")
// errPending indicates that another instance of Helm is already applying an operation on a release.
errPending = errors.New("another operation (install/upgrade/rollback) is in progress")
)
// ValidName is a regular expression for resource names.
//
// DEPRECATED: This will be removed in Helm 4, and is no longer used here. See
// pkg/lint/rules.validateMetadataNameFunc for the replacement.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
var ValidName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
// Configuration injects the dependencies that all actions share.
type Configuration struct {
// RESTClientGetter is an interface that loads Kubernetes clients.
RESTClientGetter RESTClientGetter
// Releases stores records of releases.
Releases *storage.Storage
// KubeClient is a Kubernetes API client.
KubeClient kube.Interface
// RegistryClient is a client for working with registries
RegistryClient *registry.Client
// Capabilities describes the capabilities of the Kubernetes cluster.
Capabilities *chartutil.Capabilities
Log func(string, ...interface{})
}
// renderResources renders the templates in a chart
//
// TODO: This function is badly in need of a refactor.
// TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
// This code has to do with writing files to disk.
func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) {
hs := []*release.Hook{}
b := bytes.NewBuffer(nil)
caps, err := c.getCapabilities()
if err != nil {
return hs, b, "", err
}
if ch.Metadata.KubeVersion != "" {
if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
}
}
var files map[string]string
var err2 error
// A `helm template` or `helm install --dry-run` should not talk to the remote cluster.
// It will break in interesting and exotic ways because other data (e.g. discovery)
// is mocked. It is not up to the template author to decide when the user wants to
// connect to the cluster. So when the user says to dry run, respect the user's
// wishes and do not connect to the cluster.
if !dryRun && c.RESTClientGetter != nil {
rest, err := c.RESTClientGetter.ToRESTConfig()
if err != nil {
return hs, b, "", err
}
files, err2 = engine.RenderWithClient(ch, values, rest)
} else {
files, err2 = engine.Render(ch, values)
}
if err2 != nil {
return hs, b, "", err2
}
// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
// pull it out of here into a separate file so that we can actually use the output of the rendered
// text file. We have to spin through this map because the file contains path information, so we
// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
// it in the sortHooks.
var notesBuffer bytes.Buffer
for k, v := range files {
if strings.HasSuffix(k, notesFileSuffix) {
if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) {
// If buffer contains data, add newline before adding more
if notesBuffer.Len() > 0 {
notesBuffer.WriteString("\n")
}
notesBuffer.WriteString(v)
}
delete(files, k)
}
}
notes := notesBuffer.String()
// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also
// removed here.
hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
if err != nil {
// By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes.
//
// We return the files as a big blob of data to help the user debug parser
// errors.
for name, content := range files {
if strings.TrimSpace(content) == "" {
continue
}
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
}
return hs, b, "", err
}
// Aggregate all valid manifests into one big doc.
fileWritten := make(map[string]bool)
if includeCrds {
for _, crd := range ch.CRDObjects() {
if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:]))
} else {
err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name])
if err != nil {
return hs, b, "", err
}
fileWritten[crd.Name] = true
}
}
}
for _, m := range manifests {
if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
} else {
newDir := outputDir
if useReleaseName {
newDir = filepath.Join(outputDir, releaseName)
}
// NOTE: We do not have to worry about the post-renderer because
// output dir is only used by `helm template`. In the next major
// release, we should move this logic to template only as it is not
// used by install or upgrade
err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
if err != nil {
return hs, b, "", err
}
fileWritten[m.Name] = true
}
}
if pr != nil {
b, err = pr.Run(b)
if err != nil {
return hs, b, notes, errors.Wrap(err, "error while running post render on files")
}
}
return hs, b, notes, nil
}
// RESTClientGetter gets the rest client
type RESTClientGetter interface {
ToRESTConfig() (*rest.Config, error)
ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
ToRESTMapper() (meta.RESTMapper, error)
}
// DebugLog sets the logger that writes debug strings
type DebugLog func(format string, v ...interface{})
// capabilities builds a Capabilities from discovery information.
func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
if c.Capabilities != nil {
return c.Capabilities, nil
}
dc, err := c.RESTClientGetter.ToDiscoveryClient()
if err != nil {
return nil, errors.Wrap(err, "could not get Kubernetes discovery client")
}
// force a discovery cache invalidation to always fetch the latest server version/capabilities.
dc.Invalidate()
kubeVersion, err := dc.ServerVersion()
if err != nil {
return nil, errors.Wrap(err, "could not get server version from Kubernetes")
}
// Issue #6361:
// Client-Go emits an error when an API service is registered but unimplemented.
// We trap that error here and print a warning. But since the discovery client continues
// building the API object, it is correctly populated with all valid APIs.
// See https://github.com/kubernetes/kubernetes/issues/72051#issuecomment-521157642
apiVersions, err := GetVersionSet(dc)
if err != nil {
if discovery.IsGroupDiscoveryFailedError(err) {
c.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err)
c.Log("WARNING: To fix this, kubectl delete apiservice <service-name>")
} else {
return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
}
}
c.Capabilities = &chartutil.Capabilities{
APIVersions: apiVersions,
KubeVersion: chartutil.KubeVersion{
Version: kubeVersion.GitVersion,
Major: kubeVersion.Major,
Minor: kubeVersion.Minor,
},
}
return c.Capabilities, nil
}
// KubernetesClientSet creates a new kubernetes ClientSet based on the configuration
func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
conf, err := c.RESTClientGetter.ToRESTConfig()
if err != nil {
return nil, errors.Wrap(err, "unable to generate config for kubernetes client")
}
return kubernetes.NewForConfig(conf)
}
// Now generates a timestamp
//
// If the configuration has a Timestamper on it, that will be used.
// Otherwise, this will use time.Now().
func (c *Configuration) Now() time.Time {
return Timestamper()
}
func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) {
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name)
}
if version <= 0 {
return c.Releases.Last(name)
}
return c.Releases.Get(name, version)
}
// GetVersionSet retrieves a set of available k8s API versions
func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) {
groups, resources, err := client.ServerGroupsAndResources()
if err != nil && !discovery.IsGroupDiscoveryFailedError(err) {
return chartutil.DefaultVersionSet, errors.Wrap(err, "could not get apiVersions from Kubernetes")
}
// FIXME: The Kubernetes test fixture for cli appears to always return nil
// for calls to Discovery().ServerGroupsAndResources(). So in this case, we
// return the default API list. This is also a safe value to return in any
// other odd-ball case.
if len(groups) == 0 && len(resources) == 0 {
return chartutil.DefaultVersionSet, nil
}
versionMap := make(map[string]interface{})
versions := []string{}
// Extract the groups
for _, g := range groups {
for _, gv := range g.Versions {
versionMap[gv.GroupVersion] = struct{}{}
}
}
// Extract the resources
var id string
var ok bool
for _, r := range resources {
for _, rl := range r.APIResources {
// A Kind at a GroupVersion can show up more than once. We only want
// it displayed once in the final output.
id = path.Join(r.GroupVersion, rl.Kind)
if _, ok = versionMap[id]; !ok {
versionMap[id] = struct{}{}
}
}
}
// Convert to a form that NewVersionSet can use
for k := range versionMap {
versions = append(versions, k)
}
return chartutil.VersionSet(versions), nil
}
// recordRelease with an update operation in case reuse has been set.
func (c *Configuration) recordRelease(r *release.Release) {
if err := c.Releases.Update(r); err != nil {
c.Log("warning: Failed to update release %s: %s", r.Name, err)
}
}
// Init initializes the action configuration
func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error {
kc := kube.New(getter)
kc.Log = log
lazyClient := &lazyClient{
namespace: namespace,
clientFn: kc.Factory.KubernetesClientSet,
}
var store *storage.Storage
switch helmDriver {
case "secret", "secrets", "":
d := driver.NewSecrets(newSecretClient(lazyClient))
d.Log = log
store = storage.Init(d)
case "configmap", "configmaps":
d := driver.NewConfigMaps(newConfigMapClient(lazyClient))
d.Log = log
store = storage.Init(d)
case "memory":
var d *driver.Memory
if c.Releases != nil {
if mem, ok := c.Releases.Driver.(*driver.Memory); ok {
// This function can be called more than once (e.g., helm list --all-namespaces).
// If a memory driver was already initialized, re-use it but set the possibly new namespace.
// We re-use it in case some releases where already created in the existing memory driver.
d = mem
}
}
if d == nil {
d = driver.NewMemory()
}
d.SetNamespace(namespace)
store = storage.Init(d)
case "sql":
d, err := driver.NewSQL(
os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"),
log,
namespace,
)
if err != nil {
panic(fmt.Sprintf("Unable to instantiate SQL driver: %v", err))
}
store = storage.Init(d)
default:
// Not sure what to do here.
panic("Unknown driver in HELM_DRIVER: " + helmDriver)
}
c.RESTClientGetter = getter
c.KubeClient = kc
c.Releases = store
c.Log = log
return nil
}

View File

@@ -0,0 +1,63 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"io"
"path/filepath"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chartutil"
)
// ChartExport performs a chart export operation.
type ChartExport struct {
cfg *Configuration
Destination string
}
// NewChartExport creates a new ChartExport object with the given configuration.
func NewChartExport(cfg *Configuration) *ChartExport {
return &ChartExport{
cfg: cfg,
}
}
// Run executes the chart export operation
func (a *ChartExport) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
ch, err := a.cfg.RegistryClient.LoadChart(r)
if err != nil {
return err
}
// Save the chart to local destination directory
err = chartutil.SaveDir(ch, a.Destination)
if err != nil {
return err
}
d := filepath.Join(a.Destination, ch.Metadata.Name)
fmt.Fprintf(out, "Exported chart to %s/\n", d)
return nil
}

View File

@@ -0,0 +1,38 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
)
// ChartList performs a chart list operation.
type ChartList struct {
cfg *Configuration
}
// NewChartList creates a new ChartList object with the given configuration.
func NewChartList(cfg *Configuration) *ChartList {
return &ChartList{
cfg: cfg,
}
}
// Run executes the chart list operation
func (a *ChartList) Run(out io.Writer) error {
return a.cfg.RegistryClient.PrintChartTable()
}

View File

@@ -0,0 +1,44 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartPull performs a chart pull operation.
type ChartPull struct {
cfg *Configuration
}
// NewChartPull creates a new ChartPull object with the given configuration.
func NewChartPull(cfg *Configuration) *ChartPull {
return &ChartPull{
cfg: cfg,
}
}
// Run executes the chart pull operation
func (a *ChartPull) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.PullChartToCache(r)
}

View File

@@ -0,0 +1,44 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartPush performs a chart push operation.
type ChartPush struct {
cfg *Configuration
}
// NewChartPush creates a new ChartPush object with the given configuration.
func NewChartPush(cfg *Configuration) *ChartPush {
return &ChartPush{
cfg: cfg,
}
}
// Run executes the chart push operation
func (a *ChartPush) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.PushChart(r)
}

View File

@@ -0,0 +1,44 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
)
// ChartRemove performs a chart remove operation.
type ChartRemove struct {
cfg *Configuration
}
// NewChartRemove creates a new ChartRemove object with the given configuration.
func NewChartRemove(cfg *Configuration) *ChartRemove {
return &ChartRemove{
cfg: cfg,
}
}
// Run executes the chart remove operation
func (a *ChartRemove) Run(out io.Writer, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
return a.cfg.RegistryClient.RemoveChart(r)
}

View File

@@ -0,0 +1,51 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/chart"
)
// ChartSave performs a chart save operation.
type ChartSave struct {
cfg *Configuration
}
// NewChartSave creates a new ChartSave object with the given configuration.
func NewChartSave(cfg *Configuration) *ChartSave {
return &ChartSave{
cfg: cfg,
}
}
// Run executes the chart save operation
func (a *ChartSave) Run(out io.Writer, ch *chart.Chart, ref string) error {
r, err := registry.ParseReference(ref)
if err != nil {
return err
}
// If no tag is present, use the chart version
if r.Tag == "" {
r.Tag = ch.Metadata.Version
}
return a.cfg.RegistryClient.SaveChart(ch, r)
}

View File

@@ -0,0 +1,227 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/gosuri/uitable"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
)
// Dependency is the action for building a given chart's dependency tree.
//
// It provides the implementation of 'helm dependency' and its respective subcommands.
type Dependency struct {
Verify bool
Keyring string
SkipRefresh bool
}
// NewDependency creates a new Dependency object with the given configuration.
func NewDependency() *Dependency {
return &Dependency{}
}
// List executes 'helm dependency list'.
func (d *Dependency) List(chartpath string, out io.Writer) error {
c, err := loader.Load(chartpath)
if err != nil {
return err
}
if c.Metadata.Dependencies == nil {
fmt.Fprintf(out, "WARNING: no dependencies at %s\n", filepath.Join(chartpath, "charts"))
return nil
}
d.printDependencies(chartpath, out, c)
fmt.Fprintln(out)
d.printMissing(chartpath, out, c.Metadata.Dependencies)
return nil
}
// dependencyStatus returns a string describing the status of a dependency viz a viz the parent chart.
func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart) string {
filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*")
// If a chart is unpacked, this will check the unpacked chart's `charts/` directory for tarballs.
// Technically, this is COMPLETELY unnecessary, and should be removed in Helm 4. It is here
// to preserved backward compatibility. In Helm 2/3, there is a "difference" between
// the tgz version (which outputs "ok" if it unpacks) and the loaded version (which outputs
// "unpacked"). Early in Helm 2's history, this would have made a difference. But it no
// longer does. However, since this code shipped with Helm 3, the output must remain stable
// until Helm 4.
switch archives, err := filepath.Glob(filepath.Join(chartpath, "charts", filename)); {
case err != nil:
return "bad pattern"
case len(archives) > 1:
// See if the second part is a SemVer
found := []string{}
for _, arc := range archives {
// we need to trip the prefix dirs and the extension off.
filename = strings.TrimSuffix(filepath.Base(arc), ".tgz")
maybeVersion := strings.TrimPrefix(filename, fmt.Sprintf("%s-", dep.Name))
if _, err := semver.StrictNewVersion(maybeVersion); err == nil {
// If the version parsed without an error, it is possibly a valid
// version.
found = append(found, arc)
}
}
if l := len(found); l == 1 {
// If we get here, we do the same thing as in len(archives) == 1.
if r := statArchiveForStatus(found[0], dep); r != "" {
return r
}
// Fall through and look for directories
} else if l > 1 {
return "too many matches"
}
// The sanest thing to do here is to fall through and see if we have any directory
// matches.
case len(archives) == 1:
archive := archives[0]
if r := statArchiveForStatus(archive, dep); r != "" {
return r
}
}
// End unnecessary code.
var depChart *chart.Chart
for _, item := range parent.Dependencies() {
if item.Name() == dep.Name {
depChart = item
}
}
if depChart == nil {
return "missing"
}
if depChart.Metadata.Version != dep.Version {
constraint, err := semver.NewConstraint(dep.Version)
if err != nil {
return "invalid version"
}
v, err := semver.NewVersion(depChart.Metadata.Version)
if err != nil {
return "invalid version"
}
if !constraint.Check(v) {
return "wrong version"
}
}
return "unpacked"
}
// stat an archive and return a message if the stat is successful
//
// This is a refactor of the code originally in dependencyStatus. It is here to
// support legacy behavior, and should be removed in Helm 4.
func statArchiveForStatus(archive string, dep *chart.Dependency) string {
if _, err := os.Stat(archive); err == nil {
c, err := loader.Load(archive)
if err != nil {
return "corrupt"
}
if c.Name() != dep.Name {
return "misnamed"
}
if c.Metadata.Version != dep.Version {
constraint, err := semver.NewConstraint(dep.Version)
if err != nil {
return "invalid version"
}
v, err := semver.NewVersion(c.Metadata.Version)
if err != nil {
return "invalid version"
}
if !constraint.Check(v) {
return "wrong version"
}
}
return "ok"
}
return ""
}
// printDependencies prints all of the dependencies in the yaml file.
func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) {
table := uitable.New()
table.MaxColWidth = 80
table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS")
for _, row := range c.Metadata.Dependencies {
table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row, c))
}
fmt.Fprintln(out, table)
}
// printMissing prints warnings about charts that are present on disk, but are
// not in Charts.yaml.
func (d *Dependency) printMissing(chartpath string, out io.Writer, reqs []*chart.Dependency) {
folder := filepath.Join(chartpath, "charts/*")
files, err := filepath.Glob(folder)
if err != nil {
fmt.Fprintln(out, err)
return
}
for _, f := range files {
fi, err := os.Stat(f)
if err != nil {
fmt.Fprintf(out, "Warning: %s\n", err)
}
// Skip anything that is not a directory and not a tgz file.
if !fi.IsDir() && filepath.Ext(f) != ".tgz" {
continue
}
c, err := loader.Load(f)
if err != nil {
fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f)
continue
}
found := false
for _, d := range reqs {
if d.Name == c.Name() {
found = true
break
}
}
if !found {
fmt.Fprintf(out, "WARNING: %q is not in Chart.yaml.\n", f)
}
}
}

View File

@@ -0,0 +1,22 @@
/*
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 contains the logic for each action that Helm can perform.
//
// This is a library for calling top-level Helm actions like 'install',
// 'upgrade', or 'list'. Actions approximately match the command line
// invocations that the Helm client uses.
package action

View File

@@ -0,0 +1,47 @@
/*
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 (
"helm.sh/helm/v3/pkg/release"
)
// Get is the action for checking a given release's information.
//
// It provides the implementation of 'helm get' and its respective subcommands (except `helm get values`).
type Get struct {
cfg *Configuration
// Initializing Version to 0 will get the latest revision of the release.
Version int
}
// NewGet creates a new Get object with the given configuration.
func NewGet(cfg *Configuration) *Get {
return &Get{
cfg: cfg,
}
}
// Run executes 'helm get' against the given release.
func (g *Get) Run(name string) (*release.Release, error) {
if err := g.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
return g.cfg.releaseContent(name, g.Version)
}

View File

@@ -0,0 +1,60 @@
/*
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 (
"helm.sh/helm/v3/pkg/chartutil"
)
// GetValues is the action for checking a given release's values.
//
// It provides the implementation of 'helm get values'.
type GetValues struct {
cfg *Configuration
Version int
AllValues bool
}
// NewGetValues creates a new GetValues object with the given configuration.
func NewGetValues(cfg *Configuration) *GetValues {
return &GetValues{
cfg: cfg,
}
}
// Run executes 'helm get values' against the given release.
func (g *GetValues) Run(name string) (map[string]interface{}, error) {
if err := g.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
rel, err := g.cfg.releaseContent(name, g.Version)
if err != nil {
return nil, err
}
// If the user wants all values, compute the values and return.
if g.AllValues {
cfg, err := chartutil.CoalesceValues(rel.Chart, rel.Config)
if err != nil {
return nil, err
}
return cfg, nil
}
return rel.Config, nil
}

View File

@@ -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 action
import (
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
)
// History is the action for checking the release's ledger.
//
// It provides the implementation of 'helm history'.
// It returns all the revisions for a specific release.
// To list up to one revision of every release in one specific, or in all,
// namespaces, see the List action.
type History struct {
cfg *Configuration
Max int
Version int
}
// NewHistory creates a new History object with the given configuration.
func NewHistory(cfg *Configuration) *History {
return &History{
cfg: cfg,
}
}
// Run executes 'helm history' against the given release.
func (h *History) Run(name string) ([]*release.Release, error) {
if err := h.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("release name is invalid: %s", name)
}
h.cfg.Log("getting history for release %s", name)
return h.cfg.Releases.History(name)
}

View File

@@ -0,0 +1,151 @@
/*
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 (
"bytes"
"sort"
"time"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
// execHook executes all of the hooks for the given hook event.
func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, timeout time.Duration) error {
executingHooks := []*release.Hook{}
for _, h := range rl.Hooks {
for _, e := range h.Events {
if e == hook {
executingHooks = append(executingHooks, h)
}
}
}
// hooke are pre-ordered by kind, so keep order stable
sort.Stable(hookByWeight(executingHooks))
for _, h := range executingHooks {
// Set default delete policy to before-hook-creation
if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 {
// TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion
// resources. For all other resource types update in place if a
// resource with the same name already exists and is owned by the
// current release.
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
}
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil {
return err
}
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), true)
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for %s hook %s", hook, h.Path)
}
// Record the time at which the hook was applied to the cluster
h.LastRun = release.HookExecution{
StartedAt: helmtime.Now(),
Phase: release.HookPhaseRunning,
}
cfg.recordRelease(rl)
// As long as the implementation of WatchUntilReady does not panic, HookPhaseFailed or HookPhaseSucceeded
// should always be set by this function. If we fail to do that for any reason, then HookPhaseUnknown is
// the most appropriate value to surface.
h.LastRun.Phase = release.HookPhaseUnknown
// Create hook resources
if _, err := cfg.KubeClient.Create(resources); err != nil {
h.LastRun.CompletedAt = helmtime.Now()
h.LastRun.Phase = release.HookPhaseFailed
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
}
// Watch hook resources until they have completed
err = cfg.KubeClient.WatchUntilReady(resources, timeout)
// Note the time of success/failure
h.LastRun.CompletedAt = helmtime.Now()
// Mark hook as succeeded or failed
if err != nil {
h.LastRun.Phase = release.HookPhaseFailed
// If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if err := cfg.deleteHookByPolicy(h, release.HookFailed); err != nil {
return err
}
return err
}
h.LastRun.Phase = release.HookPhaseSucceeded
}
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, h := range executingHooks {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded); err != nil {
return err
}
}
return nil
}
// hookByWeight is a sorter for hooks
type hookByWeight []*release.Hook
func (x hookByWeight) Len() int { return len(x) }
func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x hookByWeight) Less(i, j int) bool {
if x[i].Weight == x[j].Weight {
return x[i].Name < x[j].Name
}
return x[i].Weight < x[j].Weight
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error {
// Never delete CustomResourceDefinitions; this could cause lots of
// cascading garbage collection.
if h.Kind == "CustomResourceDefinition" {
return nil
}
if hookHasDeletePolicy(h, policy) {
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), false)
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for deleting hook %s", h.Path)
}
_, errs := cfg.KubeClient.Delete(resources)
if len(errs) > 0 {
return errors.New(joinErrors(errs))
}
}
return nil
}
// hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
// supported by helm. If so, mark the hook as one should be deleted.
func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool {
for _, v := range h.DeletePolicies {
if policy == v {
return true
}
}
return false
}

View File

@@ -0,0 +1,720 @@
/*
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 (
"bytes"
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/kube"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
"helm.sh/helm/v3/pkg/repo"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
)
// releaseNameMaxLen is the maximum length of a release name.
//
// As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for
// charts to add data. Effectively, that gives us 53 chars.
// See https://github.com/helm/helm/issues/1528
const releaseNameMaxLen = 53
// NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine
// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
// wants to see this file after rendering in the status command. However, it must be a suffix
// since there can be filepath in front of it.
const notesFileSuffix = "NOTES.txt"
const defaultDirectoryPermission = 0755
// Install performs an installation operation.
type Install struct {
cfg *Configuration
ChartPathOptions
ClientOnly bool
CreateNamespace bool
DryRun bool
DisableHooks bool
Replace bool
Wait bool
WaitForJobs bool
Devel bool
DependencyUpdate bool
Timeout time.Duration
Namespace string
ReleaseName string
GenerateName bool
NameTemplate string
Description string
OutputDir string
Atomic bool
SkipCRDs bool
SubNotes bool
DisableOpenAPIValidation bool
IncludeCRDs bool
// KubeVersion allows specifying a custom kubernetes version to use and
// APIVersions allows a manual set of supported API Versions to be passed
// (for things like templating). These are ignored if ClientOnly is false
KubeVersion *chartutil.KubeVersion
APIVersions chartutil.VersionSet
// Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false
IsUpgrade bool
// Used by helm template to add the release as part of OutputDir path
// OutputDir/<ReleaseName>
UseReleaseName bool
PostRenderer postrender.PostRenderer
}
// ChartPathOptions captures common options used for controlling chart paths
type ChartPathOptions struct {
CaFile string // --ca-file
CertFile string // --cert-file
KeyFile string // --key-file
InsecureSkipTLSverify bool // --insecure-skip-verify
Keyring string // --keyring
Password string // --password
PassCredentialsAll bool // --pass-credentials
RepoURL string // --repo
Username string // --username
Verify bool // --verify
Version string // --version
}
// NewInstall creates a new Install object with the given configuration.
func NewInstall(cfg *Configuration) *Install {
return &Install{
cfg: cfg,
}
}
func (i *Install) installCRDs(crds []chart.CRD) error {
// We do these one file at a time in the order they were read.
totalItems := []*resource.Info{}
for _, obj := range crds {
// Read in the resources
res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false)
if err != nil {
return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
}
// Send them to Kube
if _, err := i.cfg.KubeClient.Create(res); err != nil {
// If the error is CRD already exists, continue.
if apierrors.IsAlreadyExists(err) {
crdName := res[0].Name
i.cfg.Log("CRD %s is already present. Skipping.", crdName)
continue
}
return errors.Wrapf(err, "failed to install CRD %s", obj.Name)
}
totalItems = append(totalItems, res...)
}
if len(totalItems) > 0 {
// Invalidate the local cache, since it will not have the new CRDs
// present.
discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient()
if err != nil {
return err
}
i.cfg.Log("Clearing discovery cache")
discoveryClient.Invalidate()
// Give time for the CRD to be recognized.
if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil {
return err
}
// Make sure to force a rebuild of the cache.
discoveryClient.ServerGroups()
}
return nil
}
// Run executes the installation
//
// If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
}
if err := i.availableName(); err != nil {
return nil, err
}
// Pre-install anything in the crd/ directory. We do this before Helm
// contacts the upstream server and builds the capabilities object.
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here
if i.DryRun {
i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
} else if err := i.installCRDs(crds); err != nil {
return nil, err
}
}
if i.ClientOnly {
// Add mock objects in here so it doesn't use Kube API server
// NOTE(bacongobbler): used for `helm template`
i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy()
if i.KubeVersion != nil {
i.cfg.Capabilities.KubeVersion = *i.KubeVersion
}
i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...)
i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
mem := driver.NewMemory()
mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem)
} else if !i.ClientOnly && len(i.APIVersions) > 0 {
i.cfg.Log("API Version list given outside of client only mode, this list will be ignored")
}
if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
return nil, err
}
// Make sure if Atomic is set, that wait is set as well. This makes it so
// the user doesn't have to specify both
i.Wait = i.Wait || i.Atomic
caps, err := i.cfg.getCapabilities()
if err != nil {
return nil, err
}
//special case for helm template --is-upgrade
isUpgrade := i.IsUpgrade && i.DryRun
options := chartutil.ReleaseOptions{
Name: i.ReleaseName,
Namespace: i.Namespace,
Revision: 1,
IsInstall: !isUpgrade,
IsUpgrade: isUpgrade,
}
valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)
if err != nil {
return nil, err
}
rel := i.createRelease(chrt, vals)
var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, i.DryRun)
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()
}
// Check error from render
if err != nil {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
// Return a release with partial data so that the client can show debugging information.
return rel, err
}
// Mark this release as in-progress
rel.SetStatus(release.StatusPendingInstall, "Initial install underway")
var toBeAdopted kube.ResourceList
resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
if err != nil {
return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
}
// It is safe to use "force" here because these are resources currently rendered by the chart.
err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true))
if err != nil {
return nil, err
}
// Install requires an extra validation step of checking that resources
// don't already exist before we actually create resources. If we continue
// forward and create the release object with resources that already exist,
// we'll end up in a state where we will delete those resources upon
// deleting the release because the manifest will be pointing at that
// resource
if !i.ClientOnly && !isUpgrade && len(resources) > 0 {
toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
if err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install")
}
}
// Bail out here if it is a dry run
if i.DryRun {
rel.Info.Description = "Dry run complete"
return rel, nil
}
if i.CreateNamespace {
ns := &v1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
},
ObjectMeta: metav1.ObjectMeta{
Name: i.Namespace,
Labels: map[string]string{
"name": i.Namespace,
},
},
}
buf, err := yaml.Marshal(ns)
if err != nil {
return nil, err
}
resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true)
if err != nil {
return nil, err
}
if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) {
return nil, err
}
}
// If Replace is true, we need to supercede the last release.
if i.Replace {
if err := i.replaceRelease(rel); err != nil {
return nil, err
}
}
// Store the release in history before continuing (new in Helm 3). We always know
// that this is a create operation.
if err := i.cfg.Releases.Create(rel); err != nil {
// We could try to recover gracefully here, but since nothing has been installed
// yet, this is probably safer than trying to continue when we know storage is
// not working.
return rel, err
}
// pre-install hooks
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
}
}
// At this point, we can do the install. Note that before we were detecting whether to
// do an update, but it's not clear whether we WANT to do an update if the re-use is set
// to true, since that is basically an upgrade operation.
if len(toBeAdopted) == 0 && len(resources) > 0 {
if _, err := i.cfg.KubeClient.Create(resources); err != nil {
return i.failRelease(rel, err)
}
} else if len(resources) > 0 {
if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil {
return i.failRelease(rel, err)
}
}
if i.Wait {
if i.WaitForJobs {
if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil {
return i.failRelease(rel, err)
}
} else {
if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil {
return i.failRelease(rel, err)
}
}
}
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
}
}
if len(i.Description) > 0 {
rel.SetStatus(release.StatusDeployed, i.Description)
} else {
rel.SetStatus(release.StatusDeployed, "Install complete")
}
// This is a tricky case. The release has been created, but the result
// cannot be recorded. The truest thing to tell the user is that the
// release was created. However, the user will not be able to do anything
// further with this release.
//
// One possible strategy would be to do a timed retry to see if we can get
// this stored in the future.
if err := i.recordRelease(rel); err != nil {
i.cfg.Log("failed to record the release: %s", err)
}
return rel, nil
}
func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
if i.Atomic {
i.cfg.Log("Install failed and atomic is set, uninstalling release")
uninstall := NewUninstall(i.cfg)
uninstall.DisableHooks = i.DisableHooks
uninstall.KeepHistory = false
uninstall.Timeout = i.Timeout
if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil {
return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err)
}
return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName)
}
i.recordRelease(rel) // Ignore the error, since we have another error to deal with.
return rel, err
}
// availableName tests whether a name is available
//
// Roughly, this will return an error if name is
//
// - empty
// - too long
// - already in use, and not deleted
// - used by a deleted release, and i.Replace is false
func (i *Install) availableName() error {
start := i.ReleaseName
if start == "" {
return errors.New("name is required")
}
if len(start) > releaseNameMaxLen {
return errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen)
}
if i.DryRun {
return nil
}
h, err := i.cfg.Releases.History(start)
if err != nil || len(h) < 1 {
return nil
}
releaseutil.Reverse(h, releaseutil.SortByRevision)
rel := h[0]
if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) {
return nil
}
return errors.New("cannot re-use a name that is still in use")
}
// createRelease creates a new release object
func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release {
ts := i.cfg.Now()
return &release.Release{
Name: i.ReleaseName,
Namespace: i.Namespace,
Chart: chrt,
Config: rawVals,
Info: &release.Info{
FirstDeployed: ts,
LastDeployed: ts,
Status: release.StatusUnknown,
},
Version: 1,
}
}
// recordRelease with an update operation in case reuse has been set.
func (i *Install) recordRelease(r *release.Release) error {
// This is a legacy function which has been reduced to a oneliner. Could probably
// refactor it out.
return i.cfg.Releases.Update(r)
}
// replaceRelease replaces an older release with this one
//
// This allows us to re-use names by superseding an existing release with a new one
func (i *Install) replaceRelease(rel *release.Release) error {
hist, err := i.cfg.Releases.History(rel.Name)
if err != nil || len(hist) == 0 {
// No releases exist for this name, so we can return early
return nil
}
releaseutil.Reverse(hist, releaseutil.SortByRevision)
last := hist[0]
// Update version to the next available
rel.Version = last.Version + 1
// Do not change the status of a failed release.
if last.Info.Status == release.StatusFailed {
return nil
}
// For any other status, mark it as superseded and store the old record
last.SetStatus(release.StatusSuperseded, "superseded by new release")
return i.recordRelease(last)
}
// write the <data> to <output-dir>/<name>. <append> controls if the file is created or content will be appended
func writeToFile(outputDir string, name string, data string, append bool) error {
outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator))
err := ensureDirectoryForFile(outfileName)
if err != nil {
return err
}
f, err := createOrOpenFile(outfileName, append)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data))
if err != nil {
return err
}
fmt.Printf("wrote %s\n", outfileName)
return nil
}
func createOrOpenFile(filename string, append bool) (*os.File, error) {
if append {
return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600)
}
return os.Create(filename)
}
// check if the directory exists to create file. creates if don't exists
func ensureDirectoryForFile(file string) error {
baseDir := path.Dir(file)
_, err := os.Stat(baseDir)
if err != nil && !os.IsNotExist(err) {
return err
}
return os.MkdirAll(baseDir, defaultDirectoryPermission)
}
// NameAndChart returns the name and chart that should be used.
//
// This will read the flags and handle name generation if necessary.
func (i *Install) NameAndChart(args []string) (string, string, error) {
flagsNotSet := func() error {
if i.GenerateName {
return errors.New("cannot set --generate-name and also specify a name")
}
if i.NameTemplate != "" {
return errors.New("cannot set --name-template and also specify a name")
}
return nil
}
if len(args) > 2 {
return args[0], args[1], errors.Errorf("expected at most two arguments, unexpected arguments: %v", strings.Join(args[2:], ", "))
}
if len(args) == 2 {
return args[0], args[1], flagsNotSet()
}
if i.NameTemplate != "" {
name, err := TemplateName(i.NameTemplate)
return name, args[0], err
}
if i.ReleaseName != "" {
return i.ReleaseName, args[0], nil
}
if !i.GenerateName {
return "", args[0], errors.New("must either provide a name or specify --generate-name")
}
base := filepath.Base(args[0])
if base == "." || base == "" {
base = "chart"
}
// if present, strip out the file extension from the name
if idx := strings.Index(base, "."); idx != -1 {
base = base[0:idx]
}
return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil
}
// TemplateName renders a name template, returning the name or an error.
func TemplateName(nameTemplate string) (string, error) {
if nameTemplate == "" {
return "", nil
}
t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
if err != nil {
return "", err
}
var b bytes.Buffer
if err := t.Execute(&b, nil); err != nil {
return "", err
}
return b.String(), nil
}
// CheckDependencies checks the dependencies for a chart.
func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error {
var missing []string
OUTER:
for _, r := range reqs {
for _, d := range ch.Dependencies() {
if d.Name() == r.Name {
continue OUTER
}
}
missing = append(missing, r.Name)
}
if len(missing) > 0 {
return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", "))
}
return nil
}
// LocateChart looks for a chart directory in known places, and returns either the full path or an error.
//
// This does not ensure that the chart is well-formed; only that the requested filename exists.
//
// Order of resolution:
// - relative to current working directory
// - if path is absolute or begins with '.', error out here
// - URL
//
// If 'verify' was set on ChartPathOptions, this will attempt to also verify the chart.
func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (string, 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
}
if c.Verify {
if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil {
return "", err
}
}
return abs, nil
}
if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
return name, errors.Errorf("path %q not found", name)
}
dl := downloader.ChartDownloader{
Out: os.Stdout,
Keyring: c.Keyring,
Getters: getter.All(settings),
Options: []getter.Option{
getter.WithPassCredentialsAll(c.PassCredentialsAll),
getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify),
},
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
}
if c.Verify {
dl.Verify = downloader.VerifyAlways
}
if c.RepoURL != "" {
chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(c.RepoURL, c.Username, c.Password, name, version,
c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, c.PassCredentialsAll, getter.All(settings))
if err != nil {
return "", err
}
name = chartURL
// Only pass the user/pass on when the user has said to or when the
// location of the chart repo and the chart are the same domain.
u1, err := url.Parse(c.RepoURL)
if err != nil {
return "", err
}
u2, err := url.Parse(chartURL)
if err != nil {
return "", err
}
// Host on URL (returned from url.Parse) contains the port if present.
// This check ensures credentials are not passed between different
// services on different ports.
if c.PassCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password))
} else {
dl.Options = append(dl.Options, getter.WithBasicAuth("", ""))
}
} else {
dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password))
}
if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil {
return "", err
}
filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache)
if err == nil {
lname, err := filepath.Abs(filename)
if err != nil {
return filename, err
}
return lname, nil
} else if settings.Debug {
return filename, err
}
atVersion := ""
if version != "" {
atVersion = fmt.Sprintf(" at version %q", version)
}
return filename, errors.Errorf("failed to download %q%s (hint: running `helm repo update` may help)", name, atVersion)
}

View File

@@ -0,0 +1,197 @@
/*
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 (
"context"
"sync"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/watch"
applycorev1 "k8s.io/client-go/applyconfigurations/core/v1"
"k8s.io/client-go/kubernetes"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
// lazyClient is a workaround to deal with Kubernetes having an unstable client API.
// In Kubernetes v1.18 the defaults where removed which broke creating a
// client without an explicit configuration. ಠ_ಠ
type lazyClient struct {
// client caches an initialized kubernetes client
initClient sync.Once
client kubernetes.Interface
clientErr error
// clientFn loads a kubernetes client
clientFn func() (*kubernetes.Clientset, error)
// namespace passed to each client request
namespace string
}
func (s *lazyClient) init() error {
s.initClient.Do(func() {
s.client, s.clientErr = s.clientFn()
})
return s.clientErr
}
// secretClient implements a corev1.SecretsInterface
type secretClient struct{ *lazyClient }
var _ corev1.SecretInterface = (*secretClient)(nil)
func newSecretClient(lc *lazyClient) *secretClient {
return &secretClient{lazyClient: lc}
}
func (s *secretClient) Create(ctx context.Context, secret *v1.Secret, opts metav1.CreateOptions) (result *v1.Secret, err error) {
if err := s.init(); err != nil {
return nil, err
}
return s.client.CoreV1().Secrets(s.namespace).Create(ctx, secret, opts)
}
func (s *secretClient) Update(ctx context.Context, secret *v1.Secret, opts metav1.UpdateOptions) (*v1.Secret, error) {
if err := s.init(); err != nil {
return nil, err
}
return s.client.CoreV1().Secrets(s.namespace).Update(ctx, secret, opts)
}
func (s *secretClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
if err := s.init(); err != nil {
return err
}
return s.client.CoreV1().Secrets(s.namespace).Delete(ctx, name, opts)
}
func (s *secretClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
if err := s.init(); err != nil {
return err
}
return s.client.CoreV1().Secrets(s.namespace).DeleteCollection(ctx, opts, listOpts)
}
func (s *secretClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Secret, error) {
if err := s.init(); err != nil {
return nil, err
}
return s.client.CoreV1().Secrets(s.namespace).Get(ctx, name, opts)
}
func (s *secretClient) List(ctx context.Context, opts metav1.ListOptions) (*v1.SecretList, error) {
if err := s.init(); err != nil {
return nil, err
}
return s.client.CoreV1().Secrets(s.namespace).List(ctx, opts)
}
func (s *secretClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
if err := s.init(); err != nil {
return nil, err
}
return s.client.CoreV1().Secrets(s.namespace).Watch(ctx, opts)
}
func (s *secretClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v1.Secret, error) {
if err := s.init(); err != nil {
return nil, err
}
return s.client.CoreV1().Secrets(s.namespace).Patch(ctx, name, pt, data, opts, subresources...)
}
func (s *secretClient) Apply(ctx context.Context, secretConfiguration *applycorev1.SecretApplyConfiguration, opts metav1.ApplyOptions) (*v1.Secret, error) {
if err := s.init(); err != nil {
return nil, err
}
return s.client.CoreV1().Secrets(s.namespace).Apply(ctx, secretConfiguration, opts)
}
// configMapClient implements a corev1.ConfigMapInterface
type configMapClient struct{ *lazyClient }
var _ corev1.ConfigMapInterface = (*configMapClient)(nil)
func newConfigMapClient(lc *lazyClient) *configMapClient {
return &configMapClient{lazyClient: lc}
}
func (c *configMapClient) Create(ctx context.Context, configMap *v1.ConfigMap, opts metav1.CreateOptions) (*v1.ConfigMap, error) {
if err := c.init(); err != nil {
return nil, err
}
return c.client.CoreV1().ConfigMaps(c.namespace).Create(ctx, configMap, opts)
}
func (c *configMapClient) Update(ctx context.Context, configMap *v1.ConfigMap, opts metav1.UpdateOptions) (*v1.ConfigMap, error) {
if err := c.init(); err != nil {
return nil, err
}
return c.client.CoreV1().ConfigMaps(c.namespace).Update(ctx, configMap, opts)
}
func (c *configMapClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error {
if err := c.init(); err != nil {
return err
}
return c.client.CoreV1().ConfigMaps(c.namespace).Delete(ctx, name, opts)
}
func (c *configMapClient) DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
if err := c.init(); err != nil {
return err
}
return c.client.CoreV1().ConfigMaps(c.namespace).DeleteCollection(ctx, opts, listOpts)
}
func (c *configMapClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.ConfigMap, error) {
if err := c.init(); err != nil {
return nil, err
}
return c.client.CoreV1().ConfigMaps(c.namespace).Get(ctx, name, opts)
}
func (c *configMapClient) List(ctx context.Context, opts metav1.ListOptions) (*v1.ConfigMapList, error) {
if err := c.init(); err != nil {
return nil, err
}
return c.client.CoreV1().ConfigMaps(c.namespace).List(ctx, opts)
}
func (c *configMapClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
if err := c.init(); err != nil {
return nil, err
}
return c.client.CoreV1().ConfigMaps(c.namespace).Watch(ctx, opts)
}
func (c *configMapClient) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (*v1.ConfigMap, error) {
if err := c.init(); err != nil {
return nil, err
}
return c.client.CoreV1().ConfigMaps(c.namespace).Patch(ctx, name, pt, data, opts, subresources...)
}
func (c *configMapClient) Apply(ctx context.Context, configMap *applycorev1.ConfigMapApplyConfiguration, opts metav1.ApplyOptions) (*v1.ConfigMap, error) {
if err := c.init(); err != nil {
return nil, err
}
return c.client.CoreV1().ConfigMaps(c.namespace).Apply(ctx, configMap, opts)
}

View File

@@ -0,0 +1,118 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint"
"helm.sh/helm/v3/pkg/lint/support"
)
// Lint is the action for checking that the semantics of a chart are well-formed.
//
// It provides the implementation of 'helm lint'.
type Lint struct {
Strict bool
Namespace string
WithSubcharts bool
}
// LintResult is the result of Lint
type LintResult struct {
TotalChartsLinted int
Messages []support.Message
Errors []error
}
// NewLint creates a new Lint object with the given configuration.
func NewLint() *Lint {
return &Lint{}
}
// Run executes 'helm Lint' against the given chart.
func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
lowestTolerance := support.ErrorSev
if l.Strict {
lowestTolerance = support.WarningSev
}
result := &LintResult{}
for _, path := range paths {
linter, err := lintChart(path, vals, l.Namespace, l.Strict)
if err != nil {
result.Errors = append(result.Errors, err)
continue
}
result.Messages = append(result.Messages, linter.Messages...)
result.TotalChartsLinted++
for _, msg := range linter.Messages {
if msg.Severity >= lowestTolerance {
result.Errors = append(result.Errors, msg.Err)
}
}
}
return result
}
func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) {
var chartPath string
linter := support.Linter{}
if strings.HasSuffix(path, ".tgz") || strings.HasSuffix(path, ".tar.gz") {
tempDir, err := ioutil.TempDir("", "helm-lint")
if err != nil {
return linter, errors.Wrap(err, "unable to create temp dir to extract tarball")
}
defer os.RemoveAll(tempDir)
file, err := os.Open(path)
if err != nil {
return linter, errors.Wrap(err, "unable to open tarball")
}
defer file.Close()
if err = chartutil.Expand(tempDir, file); err != nil {
return linter, errors.Wrap(err, "unable to extract tarball")
}
files, err := ioutil.ReadDir(tempDir)
if err != nil {
return linter, errors.Wrapf(err, "unable to read temporary output directory %s", tempDir)
}
if !files[0].IsDir() {
return linter, errors.Errorf("unexpected file %s in temporary output directory %s", files[0].Name(), tempDir)
}
chartPath = filepath.Join(tempDir, files[0].Name())
} else {
chartPath = path
}
// Guard: Error out if this is not a chart.
if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil {
return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart")
}
return lint.All(chartPath, vals, namespace, strict), nil
}

View File

@@ -0,0 +1,323 @@
/*
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 (
"path"
"regexp"
"k8s.io/apimachinery/pkg/labels"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
)
// ListStates represents zero or more status codes that a list item may have set
//
// Because this is used as a bitmask filter, more than one bit can be flipped
// in the ListStates.
type ListStates uint
const (
// ListDeployed filters on status "deployed"
ListDeployed ListStates = 1 << iota
// ListUninstalled filters on status "uninstalled"
ListUninstalled
// ListUninstalling filters on status "uninstalling" (uninstall in progress)
ListUninstalling
// ListPendingInstall filters on status "pending" (deployment in progress)
ListPendingInstall
// ListPendingUpgrade filters on status "pending_upgrade" (upgrade in progress)
ListPendingUpgrade
// ListPendingRollback filters on status "pending_rollback" (rollback in progress)
ListPendingRollback
// ListSuperseded filters on status "superseded" (historical release version that is no longer deployed)
ListSuperseded
// ListFailed filters on status "failed" (release version not deployed because of error)
ListFailed
// ListUnknown filters on an unknown status
ListUnknown
)
// FromName takes a state name and returns a ListStates representation.
//
// Currently, there are only names for individual flipped bits, so the returned
// ListStates will only match one of the constants. However, it is possible that
// this behavior could change in the future.
func (s ListStates) FromName(str string) ListStates {
switch str {
case "deployed":
return ListDeployed
case "uninstalled":
return ListUninstalled
case "superseded":
return ListSuperseded
case "failed":
return ListFailed
case "uninstalling":
return ListUninstalling
case "pending-install":
return ListPendingInstall
case "pending-upgrade":
return ListPendingUpgrade
case "pending-rollback":
return ListPendingRollback
}
return ListUnknown
}
// ListAll is a convenience for enabling all list filters
const ListAll = ListDeployed | ListUninstalled | ListUninstalling | ListPendingInstall | ListPendingRollback | ListPendingUpgrade | ListSuperseded | ListFailed
// Sorter is a top-level sort
type Sorter uint
const (
// ByNameDesc sorts by descending lexicographic order
ByNameDesc Sorter = iota + 1
// ByDateAsc sorts by ascending dates (oldest updated release first)
ByDateAsc
// ByDateDesc sorts by descending dates (latest updated release first)
ByDateDesc
)
// List is the action for listing releases.
//
// It provides, for example, the implementation of 'helm list'.
// It returns no more than one revision of every release in one specific, or in
// all, namespaces.
// To list all the revisions of a specific release, see the History action.
type List struct {
cfg *Configuration
// All ignores the limit/offset
All bool
// AllNamespaces searches across namespaces
AllNamespaces bool
// Sort indicates the sort to use
//
// see pkg/releaseutil for several useful sorters
Sort Sorter
// Overrides the default lexicographic sorting
ByDate bool
SortReverse bool
// StateMask accepts a bitmask of states for items to show.
// The default is ListDeployed
StateMask ListStates
// Limit is the number of items to return per Run()
Limit int
// Offset is the starting index for the Run() call
Offset int
// Filter is a filter that is applied to the results
Filter string
Short bool
TimeFormat string
Uninstalled bool
Superseded bool
Uninstalling bool
Deployed bool
Failed bool
Pending bool
Selector string
}
// NewList constructs a new *List
func NewList(cfg *Configuration) *List {
return &List{
StateMask: ListDeployed | ListFailed,
cfg: cfg,
}
}
// Run executes the list command, returning a set of matches.
func (l *List) Run() ([]*release.Release, error) {
if err := l.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
var filter *regexp.Regexp
if l.Filter != "" {
var err error
filter, err = regexp.Compile(l.Filter)
if err != nil {
return nil, err
}
}
results, err := l.cfg.Releases.List(func(rel *release.Release) bool {
// Skip anything that doesn't match the filter.
if filter != nil && !filter.MatchString(rel.Name) {
return false
}
return true
})
if err != nil {
return nil, err
}
if results == nil {
return results, nil
}
// by definition, superseded releases are never shown if
// only the latest releases are returned. so if requested statemask
// is _only_ ListSuperseded, skip the latest release filter
if l.StateMask != ListSuperseded {
results = filterLatestReleases(results)
}
// State mask application must occur after filtering to
// latest releases, otherwise outdated entries can be returned
results = l.filterStateMask(results)
// Skip anything that doesn't match the selector
selectorObj, err := labels.Parse(l.Selector)
if err != nil {
return nil, err
}
results = l.filterSelector(results, selectorObj)
// Unfortunately, we have to sort before truncating, which can incur substantial overhead
l.sort(results)
// Guard on offset
if l.Offset >= len(results) {
return []*release.Release{}, nil
}
// Calculate the limit and offset, and then truncate results if necessary.
limit := len(results)
if l.Limit > 0 && l.Limit < limit {
limit = l.Limit
}
last := l.Offset + limit
if l := len(results); l < last {
last = l
}
results = results[l.Offset:last]
return results, err
}
// sort is an in-place sort where order is based on the value of a.Sort
func (l *List) sort(rels []*release.Release) {
if l.SortReverse {
l.Sort = ByNameDesc
}
if l.ByDate {
l.Sort = ByDateDesc
if l.SortReverse {
l.Sort = ByDateAsc
}
}
switch l.Sort {
case ByDateDesc:
releaseutil.SortByDate(rels)
case ByDateAsc:
releaseutil.Reverse(rels, releaseutil.SortByDate)
case ByNameDesc:
releaseutil.Reverse(rels, releaseutil.SortByName)
default:
releaseutil.SortByName(rels)
}
}
// filterLatestReleases returns a list scrubbed of old releases.
func filterLatestReleases(releases []*release.Release) []*release.Release {
latestReleases := make(map[string]*release.Release)
for _, rls := range releases {
name, namespace := rls.Name, rls.Namespace
key := path.Join(namespace, name)
if latestRelease, exists := latestReleases[key]; exists && latestRelease.Version > rls.Version {
continue
}
latestReleases[key] = rls
}
var list = make([]*release.Release, 0, len(latestReleases))
for _, rls := range latestReleases {
list = append(list, rls)
}
return list
}
func (l *List) filterStateMask(releases []*release.Release) []*release.Release {
desiredStateReleases := make([]*release.Release, 0)
for _, rls := range releases {
currentStatus := l.StateMask.FromName(rls.Info.Status.String())
mask := l.StateMask & currentStatus
if mask == 0 {
continue
}
desiredStateReleases = append(desiredStateReleases, rls)
}
return desiredStateReleases
}
func (l *List) filterSelector(releases []*release.Release, selector labels.Selector) []*release.Release {
desiredStateReleases := make([]*release.Release, 0)
for _, rls := range releases {
if selector.Matches(labels.Set(rls.Labels)) {
desiredStateReleases = append(desiredStateReleases, rls)
}
}
return desiredStateReleases
}
// SetStateMask calculates the state mask based on parameters.
func (l *List) SetStateMask() {
if l.All {
l.StateMask = ListAll
return
}
state := ListStates(0)
if l.Deployed {
state |= ListDeployed
}
if l.Uninstalled {
state |= ListUninstalled
}
if l.Uninstalling {
state |= ListUninstalling
}
if l.Pending {
state |= ListPendingInstall | ListPendingRollback | ListPendingUpgrade
}
if l.Failed {
state |= ListFailed
}
if l.Superseded {
state |= ListSuperseded
}
// Apply a default
if state == 0 {
state = ListDeployed | ListFailed
}
l.StateMask = state
}

View File

@@ -0,0 +1,182 @@
/*
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 (
"bufio"
"fmt"
"io/ioutil"
"os"
"syscall"
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
"golang.org/x/term"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/provenance"
)
// Package is the action for packaging a chart.
//
// It provides the implementation of 'helm package'.
type Package struct {
Sign bool
Key string
Keyring string
PassphraseFile string
Version string
AppVersion string
Destination string
DependencyUpdate bool
RepositoryConfig string
RepositoryCache string
}
// NewPackage creates a new Package object with the given configuration.
func NewPackage() *Package {
return &Package{}
}
// Run executes 'helm package' against the given chart and returns the path to the packaged chart.
func (p *Package) Run(path string, vals map[string]interface{}) (string, error) {
ch, err := loader.LoadDir(path)
if err != nil {
return "", err
}
// If version is set, modify the version.
if p.Version != "" {
ch.Metadata.Version = p.Version
}
if err := validateVersion(ch.Metadata.Version); err != nil {
return "", err
}
if p.AppVersion != "" {
ch.Metadata.AppVersion = p.AppVersion
}
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
}
} 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 p.Sign {
err = p.Clearsign(name)
}
return name, err
}
// validateVersion Verify that version is a Version, and error out if it is not.
func validateVersion(ver string) error {
if _, err := semver.NewVersion(ver); err != nil {
return err
}
return nil
}
// Clearsign signs a chart
func (p *Package) Clearsign(filename string) error {
// Load keyring
signer, err := provenance.NewFromKeyring(p.Keyring, p.Key)
if err != nil {
return err
}
passphraseFetcher := promptUser
if p.PassphraseFile != "" {
passphraseFetcher, err = passphraseFileFetcher(p.PassphraseFile, os.Stdin)
if err != nil {
return err
}
}
if err := signer.DecryptKey(passphraseFetcher); err != nil {
return err
}
sig, err := signer.ClearSign(filename)
if err != nil {
return err
}
return ioutil.WriteFile(filename+".prov", []byte(sig), 0644)
}
// promptUser implements provenance.PassphraseFetcher
func promptUser(name string) ([]byte, error) {
fmt.Printf("Password for key %q > ", name)
// syscall.Stdin is not an int in all environments and needs to be coerced
// into one there (e.g., Windows)
pw, err := term.ReadPassword(int(syscall.Stdin))
fmt.Println()
return pw, err
}
func passphraseFileFetcher(passphraseFile string, stdin *os.File) (provenance.PassphraseFetcher, error) {
file, err := openPassphraseFile(passphraseFile, stdin)
if err != nil {
return nil, err
}
defer file.Close()
reader := bufio.NewReader(file)
passphrase, _, err := reader.ReadLine()
if err != nil {
return nil, err
}
return func(name string) ([]byte, error) {
return passphrase, nil
}, nil
}
func openPassphraseFile(passphraseFile string, stdin *os.File) (*os.File, error) {
if passphraseFile == "-" {
stat, err := stdin.Stat()
if err != nil {
return nil, err
}
if (stat.Mode() & os.ModeNamedPipe) == 0 {
return nil, errors.New("specified reading passphrase from stdin, without input on stdin")
}
return stdin, nil
}
return os.Open(passphraseFile)
}

View File

@@ -0,0 +1,169 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/repo"
)
// Pull is the action for checking a given release's information.
//
// It provides the implementation of 'helm pull'.
type Pull struct {
ChartPathOptions
Settings *cli.EnvSettings // TODO: refactor this out of pkg/action
Devel bool
Untar bool
VerifyLater bool
UntarDir string
DestDir string
cfg *Configuration
}
type PullOpt func(*Pull)
func WithConfig(cfg *Configuration) PullOpt {
return func(p *Pull) {
p.cfg = cfg
}
}
// NewPull creates a new Pull object.
func NewPull() *Pull {
return NewPullWithOpts()
}
// NewPullWithOpts creates a new pull, with configuration options.
func NewPullWithOpts(opts ...PullOpt) *Pull {
p := &Pull{}
for _, fn := range opts {
fn(p)
}
return p
}
// Run executes 'helm pull' against the given release.
func (p *Pull) Run(chartRef string) (string, error) {
var out strings.Builder
c := downloader.ChartDownloader{
Out: &out,
Keyring: p.Keyring,
Verify: downloader.VerifyNever,
Getters: getter.All(p.Settings),
Options: []getter.Option{
getter.WithBasicAuth(p.Username, p.Password),
getter.WithPassCredentialsAll(p.PassCredentialsAll),
getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile),
getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify),
},
RepositoryConfig: p.Settings.RepositoryConfig,
RepositoryCache: p.Settings.RepositoryCache,
}
if strings.HasPrefix(chartRef, "oci://") {
if p.Version == "" {
return out.String(), errors.Errorf("--version flag is explicitly required for OCI registries")
}
c.Options = append(c.Options,
getter.WithRegistryClient(p.cfg.RegistryClient),
getter.WithTagName(p.Version))
}
if p.Verify {
c.Verify = downloader.VerifyAlways
} else if p.VerifyLater {
c.Verify = downloader.VerifyLater
}
// If untar is set, we fetch to a tempdir, then untar and copy after
// verification.
dest := p.DestDir
if p.Untar {
var err error
dest, err = ioutil.TempDir("", "helm-")
if err != nil {
return out.String(), errors.Wrap(err, "failed to untar")
}
defer os.RemoveAll(dest)
}
if p.RepoURL != "" {
chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, p.InsecureSkipTLSverify, p.PassCredentialsAll, getter.All(p.Settings))
if err != nil {
return out.String(), err
}
chartRef = chartURL
}
saved, v, err := c.DownloadTo(chartRef, p.Version, dest)
if err != nil {
return out.String(), err
}
if p.Verify {
for name := range v.SignedBy.Identities {
fmt.Fprintf(&out, "Signed by: %v\n", name)
}
fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", v.SignedBy.PrimaryKey.Fingerprint)
fmt.Fprintf(&out, "Chart Hash Verified: %s\n", v.FileHash)
}
// After verification, untar the chart into the requested directory.
if p.Untar {
ud := p.UntarDir
if !filepath.IsAbs(ud) {
ud = filepath.Join(p.DestDir, ud)
}
// Let udCheck to check conflict file/dir without replacing ud when untarDir is the current directory(.).
udCheck := ud
if udCheck == "." {
_, udCheck = filepath.Split(chartRef)
} else {
_, chartName := filepath.Split(chartRef)
udCheck = filepath.Join(udCheck, chartName)
}
if _, err := os.Stat(udCheck); err != nil {
if err := os.MkdirAll(udCheck, 0755); err != nil {
return out.String(), errors.Wrap(err, "failed to untar (mkdir)")
}
} else {
return out.String(), errors.Errorf("failed to untar: a file or directory with the name %s already exists", udCheck)
}
return out.String(), chartutil.ExpandFile(ud, saved)
}
return out.String(), nil
}

View File

@@ -0,0 +1,38 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
)
// RegistryLogin performs a registry login operation.
type RegistryLogin struct {
cfg *Configuration
}
// NewRegistryLogin creates a new RegistryLogin object with the given configuration.
func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
return &RegistryLogin{
cfg: cfg,
}
}
// Run executes the registry login operation
func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error {
return a.cfg.RegistryClient.Login(hostname, username, password, insecure)
}

View File

@@ -0,0 +1,38 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
)
// RegistryLogout performs a registry login operation.
type RegistryLogout struct {
cfg *Configuration
}
// NewRegistryLogout creates a new RegistryLogout object with the given configuration.
func NewRegistryLogout(cfg *Configuration) *RegistryLogout {
return &RegistryLogout{
cfg: cfg,
}
}
// Run executes the registry logout operation
func (a *RegistryLogout) Run(out io.Writer, hostname string) error {
return a.cfg.RegistryClient.Logout(hostname)
}

View File

@@ -0,0 +1,138 @@
/*
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 (
"context"
"fmt"
"io"
"time"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
)
// ReleaseTesting is the action for testing a release.
//
// It provides the implementation of 'helm test'.
type ReleaseTesting struct {
cfg *Configuration
Timeout time.Duration
// Used for fetching logs from test pods
Namespace string
Filters map[string][]string
}
// NewReleaseTesting creates a new ReleaseTesting object with the given configuration.
func NewReleaseTesting(cfg *Configuration) *ReleaseTesting {
return &ReleaseTesting{
cfg: cfg,
Filters: map[string][]string{},
}
}
// Run executes 'helm test' against the given release.
func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
if err := r.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("releaseTest: Release name is invalid: %s", name)
}
// finds the non-deleted release with the given name
rel, err := r.cfg.Releases.Last(name)
if err != nil {
return rel, err
}
skippedHooks := []*release.Hook{}
executingHooks := []*release.Hook{}
if len(r.Filters["!name"]) != 0 {
for _, h := range rel.Hooks {
if contains(r.Filters["!name"], h.Name) {
skippedHooks = append(skippedHooks, h)
} else {
executingHooks = append(executingHooks, h)
}
}
rel.Hooks = executingHooks
}
if len(r.Filters["name"]) != 0 {
executingHooks = nil
for _, h := range rel.Hooks {
if contains(r.Filters["name"], h.Name) {
executingHooks = append(executingHooks, h)
} else {
skippedHooks = append(skippedHooks, h)
}
}
rel.Hooks = executingHooks
}
if err := r.cfg.execHook(rel, release.HookTest, r.Timeout); err != nil {
rel.Hooks = append(skippedHooks, rel.Hooks...)
r.cfg.Releases.Update(rel)
return rel, err
}
rel.Hooks = append(skippedHooks, rel.Hooks...)
return rel, r.cfg.Releases.Update(rel)
}
// GetPodLogs will write the logs for all test pods in the given release into
// the given writer. These can be immediately output to the user or captured for
// other uses
func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error {
client, err := r.cfg.KubernetesClientSet()
if err != nil {
return errors.Wrap(err, "unable to get kubernetes client to fetch pod logs")
}
for _, h := range rel.Hooks {
for _, e := range h.Events {
if e == release.HookTest {
req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{})
logReader, err := req.Stream(context.Background())
if err != nil {
return errors.Wrapf(err, "unable to get pod logs for %s", h.Name)
}
fmt.Fprintf(out, "POD LOGS: %s\n", h.Name)
_, err = io.Copy(out, logReader)
fmt.Fprintln(out)
if err != nil {
return errors.Wrapf(err, "unable to write pod logs for %s", h.Name)
}
}
}
}
return nil
}
func contains(arr []string, value string) bool {
for _, item := range arr {
if item == value {
return true
}
}
return false
}

View File

@@ -0,0 +1,46 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"strings"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/releaseutil"
)
func filterManifestsToKeep(manifests []releaseutil.Manifest) (keep, remaining []releaseutil.Manifest) {
for _, m := range manifests {
if m.Head.Metadata == nil || m.Head.Metadata.Annotations == nil || len(m.Head.Metadata.Annotations) == 0 {
remaining = append(remaining, m)
continue
}
resourcePolicyType, ok := m.Head.Metadata.Annotations[kube.ResourcePolicyAnno]
if !ok {
remaining = append(remaining, m)
continue
}
resourcePolicyType = strings.ToLower(strings.TrimSpace(resourcePolicyType))
if resourcePolicyType == kube.KeepPolicy {
keep = append(keep, m)
}
}
return keep, remaining
}

View File

@@ -0,0 +1,241 @@
/*
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 (
"bytes"
"fmt"
"strings"
"time"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
// Rollback is the action for rolling back to a given release.
//
// It provides the implementation of 'helm rollback'.
type Rollback struct {
cfg *Configuration
Version int
Timeout time.Duration
Wait bool
WaitForJobs bool
DisableHooks bool
DryRun bool
Recreate bool // will (if true) recreate pods after a rollback.
Force bool // will (if true) force resource upgrade through uninstall/recreate if needed
CleanupOnFail bool
MaxHistory int // MaxHistory limits the maximum number of revisions saved per release
}
// NewRollback creates a new Rollback object with the given configuration.
func NewRollback(cfg *Configuration) *Rollback {
return &Rollback{
cfg: cfg,
}
}
// Run executes 'helm rollback' against the given release.
func (r *Rollback) Run(name string) error {
if err := r.cfg.KubeClient.IsReachable(); err != nil {
return err
}
r.cfg.Releases.MaxHistory = r.MaxHistory
r.cfg.Log("preparing rollback of %s", name)
currentRelease, targetRelease, err := r.prepareRollback(name)
if err != nil {
return err
}
if !r.DryRun {
r.cfg.Log("creating rolled back release for %s", name)
if err := r.cfg.Releases.Create(targetRelease); err != nil {
return err
}
}
r.cfg.Log("performing rollback of %s", name)
if _, err := r.performRollback(currentRelease, targetRelease); err != nil {
return err
}
if !r.DryRun {
r.cfg.Log("updating status for rolled back release for %s", name)
if err := r.cfg.Releases.Update(targetRelease); err != nil {
return err
}
}
return nil
}
// prepareRollback finds the previous release and prepares a new release object with
// the previous release's configuration
func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) {
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", name)
}
if r.Version < 0 {
return nil, nil, errInvalidRevision
}
currentRelease, err := r.cfg.Releases.Last(name)
if err != nil {
return nil, nil, err
}
previousVersion := r.Version
if r.Version == 0 {
previousVersion = currentRelease.Version - 1
}
r.cfg.Log("rolling back %s (current: v%d, target: v%d)", name, currentRelease.Version, previousVersion)
previousRelease, err := r.cfg.Releases.Get(name, previousVersion)
if err != nil {
return nil, nil, err
}
// Store a new release object with previous release's configuration
targetRelease := &release.Release{
Name: name,
Namespace: currentRelease.Namespace,
Chart: previousRelease.Chart,
Config: previousRelease.Config,
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: helmtime.Now(),
Status: release.StatusPendingRollback,
Notes: previousRelease.Info.Notes,
// Because we lose the reference to previous version elsewhere, we set the
// message here, and only override it later if we experience failure.
Description: fmt.Sprintf("Rollback to %d", previousVersion),
},
Version: currentRelease.Version + 1,
Manifest: previousRelease.Manifest,
Hooks: previousRelease.Hooks,
}
return currentRelease, targetRelease, nil
}
func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release) (*release.Release, error) {
if r.DryRun {
r.cfg.Log("dry run for %s", targetRelease.Name)
return targetRelease, nil
}
current, err := r.cfg.KubeClient.Build(bytes.NewBufferString(currentRelease.Manifest), false)
if err != nil {
return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest")
}
target, err := r.cfg.KubeClient.Build(bytes.NewBufferString(targetRelease.Manifest), false)
if err != nil {
return targetRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
}
// pre-rollback hooks
if !r.DisableHooks {
if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.Timeout); err != nil {
return targetRelease, err
}
} else {
r.cfg.Log("rollback hooks disabled for %s", targetRelease.Name)
}
results, err := r.cfg.KubeClient.Update(current, target, r.Force)
if err != nil {
msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err)
r.cfg.Log("warning: %s", msg)
currentRelease.Info.Status = release.StatusSuperseded
targetRelease.Info.Status = release.StatusFailed
targetRelease.Info.Description = msg
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
if r.CleanupOnFail {
r.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(results.Created))
_, errs := r.cfg.KubeClient.Delete(results.Created)
if errs != nil {
var errorList []string
for _, e := range errs {
errorList = append(errorList, e.Error())
}
return targetRelease, errors.Wrapf(fmt.Errorf("unable to cleanup resources: %s", strings.Join(errorList, ", ")), "an error occurred while cleaning up resources. original rollback error: %s", err)
}
r.cfg.Log("Resource cleanup complete")
}
return targetRelease, err
}
if r.Recreate {
// NOTE: Because this is not critical for a release to succeed, we just
// log if an error occurs and continue onward. If we ever introduce log
// levels, we should make these error level logs so users are notified
// that they'll need to go do the cleanup on their own
if err := recreate(r.cfg, results.Updated); err != nil {
r.cfg.Log(err.Error())
}
}
if r.Wait {
if r.WaitForJobs {
if err := r.cfg.KubeClient.WaitWithJobs(target, r.Timeout); err != nil {
targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name)
}
} else {
if err := r.cfg.KubeClient.Wait(target, r.Timeout); err != nil {
targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error()))
r.cfg.recordRelease(currentRelease)
r.cfg.recordRelease(targetRelease)
return targetRelease, errors.Wrapf(err, "release %s failed", targetRelease.Name)
}
}
}
// post-rollback hooks
if !r.DisableHooks {
if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.Timeout); err != nil {
return targetRelease, err
}
}
deployed, err := r.cfg.Releases.DeployedAll(currentRelease.Name)
if err != nil && !strings.Contains(err.Error(), "has no deployed releases") {
return nil, err
}
// Supersede all previous deployments, see issue #2941.
for _, rel := range deployed {
r.cfg.Log("superseding previous deployment %d", rel.Version)
rel.Info.Status = release.StatusSuperseded
r.cfg.recordRelease(rel)
}
targetRelease.Info.Status = release.StatusDeployed
return targetRelease, nil
}

View File

@@ -0,0 +1,130 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"strings"
"github.com/pkg/errors"
"k8s.io/cli-runtime/pkg/printers"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
)
// ShowOutputFormat is the format of the output of `helm show`
type ShowOutputFormat string
const (
// ShowAll is the format which shows all the information of a chart
ShowAll ShowOutputFormat = "all"
// ShowChart is the format which only shows the chart's definition
ShowChart ShowOutputFormat = "chart"
// ShowValues is the format which only shows the chart's values
ShowValues ShowOutputFormat = "values"
// ShowReadme is the format which only shows the chart's README
ShowReadme ShowOutputFormat = "readme"
)
var readmeFileNames = []string{"readme.md", "readme.txt", "readme"}
func (o ShowOutputFormat) String() string {
return string(o)
}
// Show is the action for checking a given release's information.
//
// It provides the implementation of 'helm show' and its respective subcommands.
type Show struct {
ChartPathOptions
Devel bool
OutputFormat ShowOutputFormat
JSONPathTemplate string
chart *chart.Chart // for testing
}
// NewShow creates a new Show object with the given configuration.
func NewShow(output ShowOutputFormat) *Show {
return &Show{
OutputFormat: output,
}
}
// Run executes 'helm show' against the given release.
func (s *Show) Run(chartpath string) (string, error) {
if s.chart == nil {
chrt, err := loader.Load(chartpath)
if err != nil {
return "", err
}
s.chart = chrt
}
cf, err := yaml.Marshal(s.chart.Metadata)
if err != nil {
return "", err
}
var out strings.Builder
if s.OutputFormat == ShowChart || s.OutputFormat == ShowAll {
fmt.Fprintf(&out, "%s\n", cf)
}
if (s.OutputFormat == ShowValues || s.OutputFormat == ShowAll) && s.chart.Values != nil {
if s.OutputFormat == ShowAll {
fmt.Fprintln(&out, "---")
}
if s.JSONPathTemplate != "" {
printer, err := printers.NewJSONPathPrinter(s.JSONPathTemplate)
if err != nil {
return "", errors.Wrapf(err, "error parsing jsonpath %s", s.JSONPathTemplate)
}
printer.Execute(&out, s.chart.Values)
} else {
for _, f := range s.chart.Raw {
if f.Name == chartutil.ValuesfileName {
fmt.Fprintln(&out, string(f.Data))
}
}
}
}
if s.OutputFormat == ShowReadme || s.OutputFormat == ShowAll {
if s.OutputFormat == ShowAll {
fmt.Fprintln(&out, "---")
}
readme := findReadme(s.chart.Files)
if readme == nil {
return out.String(), nil
}
fmt.Fprintf(&out, "%s\n", readme.Data)
}
return out.String(), nil
}
func findReadme(files []*chart.File) (file *chart.File) {
for _, file := range files {
for _, n := range readmeFileNames {
if strings.EqualFold(file.Name, n) {
return file
}
}
}
return nil
}

View File

@@ -0,0 +1,51 @@
/*
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 (
"helm.sh/helm/v3/pkg/release"
)
// Status is the action for checking the deployment status of releases.
//
// It provides the implementation of 'helm status'.
type Status struct {
cfg *Configuration
Version int
// If true, display description to output format,
// only affect print type table.
// TODO Helm 4: Remove this flag and output the description by default.
ShowDescription bool
}
// NewStatus creates a new Status object with the given configuration.
func NewStatus(cfg *Configuration) *Status {
return &Status{
cfg: cfg,
}
}
// Run executes 'helm status' against the given release.
func (s *Status) Run(name string) (*release.Release, error) {
if err := s.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
return s.cfg.releaseContent(name, s.Version)
}

View File

@@ -0,0 +1,212 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"strings"
"time"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
helmtime "helm.sh/helm/v3/pkg/time"
)
// Uninstall is the action for uninstalling releases.
//
// It provides the implementation of 'helm uninstall'.
type Uninstall struct {
cfg *Configuration
DisableHooks bool
DryRun bool
KeepHistory bool
Timeout time.Duration
Description string
}
// NewUninstall creates a new Uninstall object with the given configuration.
func NewUninstall(cfg *Configuration) *Uninstall {
return &Uninstall{
cfg: cfg,
}
}
// Run uninstalls the given release.
func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) {
if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
if u.DryRun {
// In the dry run case, just see if the release exists
r, err := u.cfg.releaseContent(name, 0)
if err != nil {
return &release.UninstallReleaseResponse{}, err
}
return &release.UninstallReleaseResponse{Release: r}, nil
}
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("uninstall: Release name is invalid: %s", name)
}
rels, err := u.cfg.Releases.History(name)
if err != nil {
return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", name)
}
if len(rels) < 1 {
return nil, errMissingRelease
}
releaseutil.SortByRevision(rels)
rel := rels[len(rels)-1]
// TODO: Are there any cases where we want to force a delete even if it's
// already marked deleted?
if rel.Info.Status == release.StatusUninstalled {
if !u.KeepHistory {
if err := u.purgeReleases(rels...); err != nil {
return nil, errors.Wrap(err, "uninstall: Failed to purge the release")
}
return &release.UninstallReleaseResponse{Release: rel}, nil
}
return nil, errors.Errorf("the release named %q is already deleted", name)
}
u.cfg.Log("uninstall: Deleting %s", name)
rel.Info.Status = release.StatusUninstalling
rel.Info.Deleted = helmtime.Now()
rel.Info.Description = "Deletion in progress (or silently failed)"
res := &release.UninstallReleaseResponse{Release: rel}
if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPreDelete, u.Timeout); err != nil {
return res, err
}
} else {
u.cfg.Log("delete hooks disabled for %s", name)
}
// From here on out, the release is currently considered to be in StatusUninstalling
// state.
if err := u.cfg.Releases.Update(rel); err != nil {
u.cfg.Log("uninstall: Failed to store updated release: %s", err)
}
kept, errs := u.deleteRelease(rel)
if kept != "" {
kept = "These resources were kept due to the resource policy:\n" + kept
}
res.Info = kept
if !u.DisableHooks {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil {
errs = append(errs, err)
}
}
rel.Info.Status = release.StatusUninstalled
if len(u.Description) > 0 {
rel.Info.Description = u.Description
} else {
rel.Info.Description = "Uninstallation complete"
}
if !u.KeepHistory {
u.cfg.Log("purge requested for %s", name)
err := u.purgeReleases(rels...)
if err != nil {
errs = append(errs, errors.Wrap(err, "uninstall: Failed to purge the release"))
}
// Return the errors that occurred while deleting the release, if any
if len(errs) > 0 {
return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs))
}
return res, nil
}
if err := u.cfg.Releases.Update(rel); err != nil {
u.cfg.Log("uninstall: Failed to store updated release: %s", err)
}
if len(errs) > 0 {
return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs))
}
return res, nil
}
func (u *Uninstall) purgeReleases(rels ...*release.Release) error {
for _, rel := range rels {
if _, err := u.cfg.Releases.Delete(rel.Name, rel.Version); err != nil {
return err
}
}
return nil
}
func joinErrors(errs []error) string {
es := make([]string, 0, len(errs))
for _, e := range errs {
es = append(es, e.Error())
}
return strings.Join(es, "; ")
}
// deleteRelease deletes the release and returns manifests that were kept in the deletion process
func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
var errs []error
caps, err := u.cfg.getCapabilities()
if err != nil {
return rel.Manifest, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")}
}
manifests := releaseutil.SplitManifests(rel.Manifest)
_, files, err := releaseutil.SortManifests(manifests, caps.APIVersions, releaseutil.UninstallOrder)
if err != nil {
// We could instead just delete everything in no particular order.
// FIXME: One way to delete at this point would be to try a label-based
// deletion. The problem with this is that we could get a false positive
// and delete something that was not legitimately part of this release.
return rel.Manifest, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")}
}
filesToKeep, filesToDelete := filterManifestsToKeep(files)
var kept string
for _, f := range filesToKeep {
kept += "[" + f.Head.Kind + "] " + f.Head.Metadata.Name + "\n"
}
var builder strings.Builder
for _, file := range filesToDelete {
builder.WriteString("\n---\n" + file.Content)
}
resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false)
if err != nil {
return "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
}
if len(resources) > 0 {
_, errs = u.cfg.KubeClient.Delete(resources)
}
return kept, errs
}

View File

@@ -0,0 +1,509 @@
/*
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 (
"bytes"
"context"
"fmt"
"strings"
"time"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/postrender"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil"
"helm.sh/helm/v3/pkg/storage/driver"
)
// Upgrade is the action for upgrading releases.
//
// It provides the implementation of 'helm upgrade'.
type Upgrade struct {
cfg *Configuration
ChartPathOptions
// Install is a purely informative flag that indicates whether this upgrade was done in "install" mode.
//
// Applications may use this to determine whether this Upgrade operation was done as part of a
// pure upgrade (Upgrade.Install == false) or as part of an install-or-upgrade operation
// (Upgrade.Install == true).
//
// Setting this to `true` will NOT cause `Upgrade` to perform an install if the release does not exist.
// That process must be handled by creating an Install action directly. See cmd/upgrade.go for an
// example of how this flag is used.
Install bool
// Devel indicates that the operation is done in devel mode.
Devel bool
// Namespace is the namespace in which this operation should be performed.
Namespace string
// SkipCRDs skips installing CRDs when install flag is enabled during upgrade
SkipCRDs bool
// Timeout is the timeout for this operation
Timeout time.Duration
// Wait determines whether the wait operation should be performed after the upgrade is requested.
Wait bool
// WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested.
WaitForJobs bool
// DisableHooks disables hook processing if set to true.
DisableHooks bool
// DryRun controls whether the operation is prepared, but not executed.
// If `true`, the upgrade is prepared but not performed.
DryRun bool
// Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway.
//
// This should be used with caution.
Force bool
// ResetValues will reset the values to the chart's built-ins rather than merging with existing.
ResetValues bool
// ReuseValues will re-use the user's last supplied values.
ReuseValues bool
// Recreate will (if true) recreate pods after a rollback.
Recreate bool
// MaxHistory limits the maximum number of revisions saved per release
MaxHistory int
// Atomic, if true, will roll back on failure.
Atomic bool
// CleanupOnFail will, if true, cause the upgrade to delete newly-created resources on a failed update.
CleanupOnFail bool
// SubNotes determines whether sub-notes are rendered in the chart.
SubNotes bool
// Description is the description of this operation
Description string
// PostRender is an optional post-renderer
//
// If this is non-nil, then after templates are rendered, they will be sent to the
// post renderer before sending to the Kubernetes API server.
PostRenderer postrender.PostRenderer
// DisableOpenAPIValidation controls whether OpenAPI validation is enforced.
DisableOpenAPIValidation bool
}
// NewUpgrade creates a new Upgrade object with the given configuration.
func NewUpgrade(cfg *Configuration) *Upgrade {
return &Upgrade{
cfg: cfg,
}
}
// Run executes the upgrade on the given release.
func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
if err := u.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
// Make sure if Atomic is set, that wait is set as well. This makes it so
// the user doesn't have to specify both
u.Wait = u.Wait || u.Atomic
if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("release name is invalid: %s", name)
}
u.cfg.Log("preparing upgrade for %s", name)
currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals)
if err != nil {
return nil, err
}
u.cfg.Releases.MaxHistory = u.MaxHistory
u.cfg.Log("performing update for %s", name)
res, err := u.performUpgrade(currentRelease, upgradedRelease)
if err != nil {
return res, err
}
if !u.DryRun {
u.cfg.Log("updating status for upgraded release for %s", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err
}
}
return res, nil
}
// prepareUpgrade builds an upgraded release for an upgrade operation.
func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) {
if chart == nil {
return nil, nil, errMissingChart
}
// finds the last non-deleted release with the given name
lastRelease, err := u.cfg.Releases.Last(name)
if err != nil {
// to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist
if errors.Is(err, driver.ErrReleaseNotFound) {
return nil, nil, driver.NewErrNoDeployedReleases(name)
}
return nil, nil, err
}
// Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock.
if lastRelease.Info.Status.IsPending() {
return nil, nil, errPending
}
var currentRelease *release.Release
if lastRelease.Info.Status == release.StatusDeployed {
// no need to retrieve the last deployed release from storage as the last release is deployed
currentRelease = lastRelease
} else {
// finds the deployed release with the given name
currentRelease, err = u.cfg.Releases.Deployed(name)
if err != nil {
if errors.Is(err, driver.ErrNoDeployedReleases) &&
(lastRelease.Info.Status == release.StatusFailed || lastRelease.Info.Status == release.StatusSuperseded) {
currentRelease = lastRelease
} else {
return nil, nil, err
}
}
}
// determine if values will be reused
vals, err = u.reuseValues(chart, currentRelease, vals)
if err != nil {
return nil, nil, err
}
if err := chartutil.ProcessDependencies(chart, vals); err != nil {
return nil, nil, err
}
// Increment revision count. This is passed to templates, and also stored on
// the release object.
revision := lastRelease.Version + 1
options := chartutil.ReleaseOptions{
Name: name,
Namespace: currentRelease.Namespace,
Revision: revision,
IsUpgrade: true,
}
caps, err := u.cfg.getCapabilities()
if err != nil {
return nil, nil, err
}
valuesToRender, err := chartutil.ToRenderValues(chart, vals, options, caps)
if err != nil {
return nil, nil, err
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun)
if err != nil {
return nil, nil, err
}
// Store an upgraded release.
upgradedRelease := &release.Release{
Name: name,
Namespace: currentRelease.Namespace,
Chart: chart,
Config: vals,
Info: &release.Info{
FirstDeployed: currentRelease.Info.FirstDeployed,
LastDeployed: Timestamper(),
Status: release.StatusPendingUpgrade,
Description: "Preparing upgrade", // This should be overwritten later.
},
Version: revision,
Manifest: manifestDoc.String(),
Hooks: hooks,
}
if len(notesTxt) > 0 {
upgradedRelease.Info.Notes = notesTxt
}
err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes(), !u.DisableOpenAPIValidation)
return currentRelease, upgradedRelease, err
}
func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Release) (*release.Release, error) {
current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false)
if err != nil {
// Checking for removed Kubernetes API error so can provide a more informative error message to the user
// Ref: https://github.com/helm/helm/issues/7219
if strings.Contains(err.Error(), "unable to recognize \"\": no matches for kind") {
return upgradedRelease, errors.Wrap(err, "current release manifest contains removed kubernetes api(s) for this "+
"kubernetes version and it is therefore unable to build the kubernetes "+
"objects for performing the diff. error from kubernetes")
}
return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest")
}
target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), !u.DisableOpenAPIValidation)
if err != nil {
return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest")
}
// It is safe to use force only on target because these are resources currently rendered by the chart.
err = target.Visit(setMetadataVisitor(upgradedRelease.Name, upgradedRelease.Namespace, true))
if err != nil {
return upgradedRelease, err
}
// Do a basic diff using gvk + name to figure out what new resources are being created so we can validate they don't already exist
existingResources := make(map[string]bool)
for _, r := range current {
existingResources[objectKey(r)] = true
}
var toBeCreated kube.ResourceList
for _, r := range target {
if !existingResources[objectKey(r)] {
toBeCreated = append(toBeCreated, r)
}
}
toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace)
if err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with update")
}
toBeUpdated.Visit(func(r *resource.Info, err error) error {
if err != nil {
return err
}
current.Append(r)
return nil
})
if u.DryRun {
u.cfg.Log("dry run for %s", upgradedRelease.Name)
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description
} else {
upgradedRelease.Info.Description = "Dry run complete"
}
return upgradedRelease, nil
}
u.cfg.Log("creating upgraded release for %s", upgradedRelease.Name)
if err := u.cfg.Releases.Create(upgradedRelease); err != nil {
return nil, err
}
// pre-upgrade hooks
if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil {
return u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
}
} else {
u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name)
}
results, err := u.cfg.KubeClient.Update(current, target, u.Force)
if err != nil {
u.cfg.recordRelease(originalRelease)
return u.failRelease(upgradedRelease, results.Created, err)
}
if u.Recreate {
// NOTE: Because this is not critical for a release to succeed, we just
// log if an error occurs and continue onward. If we ever introduce log
// levels, we should make these error level logs so users are notified
// that they'll need to go do the cleanup on their own
if err := recreate(u.cfg, results.Updated); err != nil {
u.cfg.Log(err.Error())
}
}
if u.Wait {
if u.WaitForJobs {
if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
return u.failRelease(upgradedRelease, results.Created, err)
}
} else {
if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
return u.failRelease(upgradedRelease, results.Created, err)
}
}
}
// post-upgrade hooks
if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil {
return u.failRelease(upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
}
}
originalRelease.Info.Status = release.StatusSuperseded
u.cfg.recordRelease(originalRelease)
upgradedRelease.Info.Status = release.StatusDeployed
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description
} else {
upgradedRelease.Info.Description = "Upgrade complete"
}
return upgradedRelease, nil
}
func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) {
msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err)
u.cfg.Log("warning: %s", msg)
rel.Info.Status = release.StatusFailed
rel.Info.Description = msg
u.cfg.recordRelease(rel)
if u.CleanupOnFail && len(created) > 0 {
u.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(created))
_, errs := u.cfg.KubeClient.Delete(created)
if errs != nil {
var errorList []string
for _, e := range errs {
errorList = append(errorList, e.Error())
}
return rel, errors.Wrapf(fmt.Errorf("unable to cleanup resources: %s", strings.Join(errorList, ", ")), "an error occurred while cleaning up resources. original upgrade error: %s", err)
}
u.cfg.Log("Resource cleanup complete")
}
if u.Atomic {
u.cfg.Log("Upgrade failed and atomic is set, rolling back to last successful release")
// As a protection, get the last successful release before rollback.
// If there are no successful releases, bail out
hist := NewHistory(u.cfg)
fullHistory, herr := hist.Run(rel.Name)
if herr != nil {
return rel, errors.Wrapf(herr, "an error occurred while finding last successful release. original upgrade error: %s", err)
}
// There isn't a way to tell if a previous release was successful, but
// generally failed releases do not get superseded unless the next
// release is successful, so this should be relatively safe
filteredHistory := releaseutil.FilterFunc(func(r *release.Release) bool {
return r.Info.Status == release.StatusSuperseded || r.Info.Status == release.StatusDeployed
}).Filter(fullHistory)
if len(filteredHistory) == 0 {
return rel, errors.Wrap(err, "unable to find a previously successful release when attempting to rollback. original upgrade error")
}
releaseutil.Reverse(filteredHistory, releaseutil.SortByRevision)
rollin := NewRollback(u.cfg)
rollin.Version = filteredHistory[0].Version
rollin.Wait = true
rollin.WaitForJobs = u.WaitForJobs
rollin.DisableHooks = u.DisableHooks
rollin.Recreate = u.Recreate
rollin.Force = u.Force
rollin.Timeout = u.Timeout
if rollErr := rollin.Run(rel.Name); rollErr != nil {
return rel, errors.Wrapf(rollErr, "an error occurred while rolling back the release. original upgrade error: %s", err)
}
return rel, errors.Wrapf(err, "release %s failed, and has been rolled back due to atomic being set", rel.Name)
}
return rel, err
}
// reuseValues copies values from the current release to a new release if the
// new release does not have any values.
//
// If the request already has values, or if there are no values in the current
// release, this does nothing.
//
// This is skipped if the u.ResetValues flag is set, in which case the
// request values are not altered.
func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) {
if u.ResetValues {
// If ResetValues is set, we completely ignore current.Config.
u.cfg.Log("resetting values to the chart's original version")
return newVals, nil
}
// If the ReuseValues flag is set, we always copy the old values over the new config's values.
if u.ReuseValues {
u.cfg.Log("reusing the old release's values")
// We have to regenerate the old coalesced values:
oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config)
if err != nil {
return nil, errors.Wrap(err, "failed to rebuild old values")
}
newVals = chartutil.CoalesceTables(newVals, current.Config)
chart.Values = oldVals
return newVals, nil
}
if len(newVals) == 0 && len(current.Config) > 0 {
u.cfg.Log("copying values from %s (v%d) to new release.", current.Name, current.Version)
newVals = current.Config
}
return newVals, nil
}
func validateManifest(c kube.Interface, manifest []byte, openAPIValidation bool) error {
_, err := c.Build(bytes.NewReader(manifest), openAPIValidation)
return err
}
// recreate captures all the logic for recreating pods for both upgrade and
// rollback. If we end up refactoring rollback to use upgrade, this can just be
// made an unexported method on the upgrade action.
func recreate(cfg *Configuration, resources kube.ResourceList) error {
for _, res := range resources {
versioned := kube.AsVersioned(res)
selector, err := kube.SelectorsForObject(versioned)
if err != nil {
// If no selector is returned, it means this object is
// definitely not a pod, so continue onward
continue
}
client, err := cfg.KubernetesClientSet()
if err != nil {
return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
}
pods, err := client.CoreV1().Pods(res.Namespace).List(context.Background(), metav1.ListOptions{
LabelSelector: selector.String(),
})
if err != nil {
return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
}
// Restart pods
for _, pod := range pods.Items {
// Delete each pod for get them restarted with changed spec.
if err := client.CoreV1().Pods(pod.Namespace).Delete(context.Background(), pod.Name, *metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil {
return errors.Wrapf(err, "unable to recreate pods for object %s/%s because an error occurred", res.Namespace, res.Name)
}
}
}
return nil
}
func objectKey(r *resource.Info) string {
gvk := r.Object.GetObjectKind().GroupVersionKind()
return fmt.Sprintf("%s/%s/%s/%s", gvk.GroupVersion().String(), gvk.Kind, r.Namespace, r.Name)
}

View File

@@ -0,0 +1,184 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v3/pkg/kube"
)
var accessor = meta.NewAccessor()
const (
appManagedByLabel = "app.kubernetes.io/managed-by"
appManagedByHelm = "Helm"
helmReleaseNameAnnotation = "meta.helm.sh/release-name"
helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace"
)
func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) {
var requireUpdate kube.ResourceList
err := resources.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
helper := resource.NewHelper(info.Client, info.Mapping)
existing, err := helper.Get(info.Namespace, info.Name)
if err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return errors.Wrap(err, "could not get information about the resource")
}
// Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace.
if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil {
return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err)
}
requireUpdate.Append(info)
return nil
})
return requireUpdate, err
}
func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) error {
lbls, err := accessor.Labels(obj)
if err != nil {
return err
}
annos, err := accessor.Annotations(obj)
if err != nil {
return err
}
var errs []error
if err := requireValue(lbls, appManagedByLabel, appManagedByHelm); err != nil {
errs = append(errs, fmt.Errorf("label validation error: %s", err))
}
if err := requireValue(annos, helmReleaseNameAnnotation, releaseName); err != nil {
errs = append(errs, fmt.Errorf("annotation validation error: %s", err))
}
if err := requireValue(annos, helmReleaseNamespaceAnnotation, releaseNamespace); err != nil {
errs = append(errs, fmt.Errorf("annotation validation error: %s", err))
}
if len(errs) > 0 {
err := errors.New("invalid ownership metadata")
for _, e := range errs {
err = fmt.Errorf("%w; %s", err, e)
}
return err
}
return nil
}
func requireValue(meta map[string]string, k, v string) error {
actual, ok := meta[k]
if !ok {
return fmt.Errorf("missing key %q: must be set to %q", k, v)
}
if actual != v {
return fmt.Errorf("key %q must equal %q: current value is %q", k, v, actual)
}
return nil
}
// setMetadataVisitor adds release tracking metadata to all resources. If force is enabled, existing
// ownership metadata will be overwritten. Otherwise an error will be returned if any resource has an
// existing and conflicting value for the managed by label or Helm release/namespace annotations.
func setMetadataVisitor(releaseName, releaseNamespace string, force bool) resource.VisitorFunc {
return func(info *resource.Info, err error) error {
if err != nil {
return err
}
if !force {
if err := checkOwnership(info.Object, releaseName, releaseNamespace); err != nil {
return fmt.Errorf("%s cannot be owned: %s", resourceString(info), err)
}
}
if err := mergeLabels(info.Object, map[string]string{
appManagedByLabel: appManagedByHelm,
}); err != nil {
return fmt.Errorf(
"%s labels could not be updated: %s",
resourceString(info), err,
)
}
if err := mergeAnnotations(info.Object, map[string]string{
helmReleaseNameAnnotation: releaseName,
helmReleaseNamespaceAnnotation: releaseNamespace,
}); err != nil {
return fmt.Errorf(
"%s annotations could not be updated: %s",
resourceString(info), err,
)
}
return nil
}
}
func resourceString(info *resource.Info) string {
_, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind()
return fmt.Sprintf(
"%s %q in namespace %q",
k, info.Name, info.Namespace,
)
}
func mergeLabels(obj runtime.Object, labels map[string]string) error {
current, err := accessor.Labels(obj)
if err != nil {
return err
}
return accessor.SetLabels(obj, mergeStrStrMaps(current, labels))
}
func mergeAnnotations(obj runtime.Object, annotations map[string]string) error {
current, err := accessor.Annotations(obj)
if err != nil {
return err
}
return accessor.SetAnnotations(obj, mergeStrStrMaps(current, annotations))
}
// merge two maps, always taking the value on the right
func mergeStrStrMaps(current, desired map[string]string) map[string]string {
result := make(map[string]string)
for k, v := range current {
result[k] = v
}
for k, desiredVal := range desired {
result[k] = desiredVal
}
return result
}

View File

@@ -0,0 +1,59 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"strings"
"helm.sh/helm/v3/pkg/downloader"
)
// Verify is the action for building a given chart's Verify tree.
//
// It provides the implementation of 'helm verify'.
type Verify struct {
Keyring string
Out string
}
// NewVerify creates a new Verify object with the given configuration.
func NewVerify() *Verify {
return &Verify{}
}
// Run executes 'helm verify'.
func (v *Verify) Run(chartfile string) error {
var out strings.Builder
p, err := downloader.VerifyChart(chartfile, v.Keyring)
if err != nil {
return err
}
for name := range p.SignedBy.Identities {
fmt.Fprintf(&out, "Signed by: %v\n", name)
}
fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint)
fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash)
// TODO(mattfarina): The output is set as a property rather than returned
// to maintain the Go API. In Helm v4 this function should return the out
// and the property on the struct can be removed.
v.Out = out.String()
return nil
}

View File

@@ -49,6 +49,23 @@ type Dependency struct {
Alias string `json:"alias,omitempty"`
}
// Validate checks for common problems with the dependency datastructure in
// the chart. This check must be done at load time before the dependency's charts are
// loaded.
func (d *Dependency) Validate() error {
d.Name = sanitizeString(d.Name)
d.Version = sanitizeString(d.Version)
d.Repository = sanitizeString(d.Repository)
d.Condition = sanitizeString(d.Condition)
for i := range d.Tags {
d.Tags[i] = sanitizeString(d.Tags[i])
}
if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) {
return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name)
}
return nil
}
// Lock is a lock file for dependencies.
//
// It represents the state that the dependencies should be in.

View File

@@ -15,6 +15,13 @@ limitations under the License.
package chart
import (
"strings"
"unicode"
"github.com/Masterminds/semver/v3"
)
// Maintainer describes a Chart maintainer.
type Maintainer struct {
// Name is a user name or organization name
@@ -25,15 +32,23 @@ type Maintainer struct {
URL string `json:"url,omitempty"`
}
// Validate checks valid data and sanitizes string characters.
func (m *Maintainer) Validate() error {
m.Name = sanitizeString(m.Name)
m.Email = sanitizeString(m.Email)
m.URL = sanitizeString(m.URL)
return nil
}
// Metadata for a Chart file. This models the structure of a Chart.yaml file.
type Metadata struct {
// The name of the chart
// The name of the chart. Required.
Name string `json:"name,omitempty"`
// The URL to a relevant project page, git repo, or contact person
Home string `json:"home,omitempty"`
// Source is the URL to the source code of this chart
Sources []string `json:"sources,omitempty"`
// A SemVer 2 conformant version string of the chart
// A SemVer 2 conformant version string of the chart. Required.
Version string `json:"version,omitempty"`
// A one-sentence description of the chart
Description string `json:"description,omitempty"`
@@ -43,7 +58,7 @@ type Metadata struct {
Maintainers []*Maintainer `json:"maintainers,omitempty"`
// The URL to an icon file.
Icon string `json:"icon,omitempty"`
// The API Version of this chart.
// The API Version of this chart. Required.
APIVersion string `json:"apiVersion,omitempty"`
// The condition to check to enable chart
Condition string `json:"condition,omitempty"`
@@ -64,11 +79,28 @@ type Metadata struct {
Type string `json:"type,omitempty"`
}
// Validate checks the metadata for known issues, returning an error if metadata is not correct
// Validate checks the metadata for known issues and sanitizes string
// characters.
func (md *Metadata) Validate() error {
if md == nil {
return ValidationError("chart.metadata is required")
}
md.Name = sanitizeString(md.Name)
md.Description = sanitizeString(md.Description)
md.Home = sanitizeString(md.Home)
md.Icon = sanitizeString(md.Icon)
md.Condition = sanitizeString(md.Condition)
md.Tags = sanitizeString(md.Tags)
md.AppVersion = sanitizeString(md.AppVersion)
md.KubeVersion = sanitizeString(md.KubeVersion)
for i := range md.Sources {
md.Sources[i] = sanitizeString(md.Sources[i])
}
for i := range md.Keywords {
md.Keywords[i] = sanitizeString(md.Keywords[i])
}
if md.APIVersion == "" {
return ValidationError("chart.metadata.apiVersion is required")
}
@@ -78,19 +110,26 @@ func (md *Metadata) Validate() error {
if md.Version == "" {
return ValidationError("chart.metadata.version is required")
}
if !isValidSemver(md.Version) {
return ValidationErrorf("chart.metadata.version %q is invalid", md.Version)
}
if !isValidChartType(md.Type) {
return ValidationError("chart.metadata.type must be application or library")
}
for _, m := range md.Maintainers {
if err := m.Validate(); err != nil {
return err
}
}
// Aliases need to be validated here to make sure that the alias name does
// not contain any illegal characters.
for _, dependency := range md.Dependencies {
if err := validateDependency(dependency); err != nil {
if err := dependency.Validate(); err != nil {
return err
}
}
// TODO validate valid semver here?
return nil
}
@@ -102,12 +141,20 @@ func isValidChartType(in string) bool {
return false
}
// validateDependency checks for common problems with the dependency datastructure in
// the chart. This check must be done at load time before the dependency's charts are
// loaded.
func validateDependency(dep *Dependency) error {
if len(dep.Alias) > 0 && !aliasNameFormat.MatchString(dep.Alias) {
return ValidationErrorf("dependency %q has disallowed characters in the alias", dep.Name)
}
return nil
func isValidSemver(v string) bool {
_, err := semver.NewVersion(v)
return err == nil
}
// sanitizeString normalize spaces and removes non-printable characters.
func sanitizeString(str string) string {
return strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return ' '
}
if unicode.IsPrint(r) {
return r
}
return -1
}, str)
}

View File

@@ -19,6 +19,7 @@ import (
"fmt"
"strconv"
"github.com/Masterminds/semver/v3"
"k8s.io/client-go/kubernetes/scheme"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@@ -58,6 +59,14 @@ type Capabilities struct {
HelmVersion helmversion.BuildInfo
}
func (capabilities *Capabilities) Copy() *Capabilities {
return &Capabilities{
KubeVersion: capabilities.KubeVersion,
APIVersions: capabilities.APIVersions,
HelmVersion: capabilities.HelmVersion,
}
}
// KubeVersion is the Kubernetes version.
type KubeVersion struct {
Version string // Kubernetes version
@@ -73,6 +82,19 @@ func (kv *KubeVersion) String() string { return kv.Version }
// Deprecated: use KubeVersion.Version.
func (kv *KubeVersion) GitVersion() string { return kv.Version }
// ParseKubeVersion parses kubernetes version from string
func ParseKubeVersion(version string) (*KubeVersion, error) {
sv, err := semver.NewVersion(version)
if err != nil {
return nil, err
}
return &KubeVersion{
Version: "v" + sv.String(),
Major: strconv.FormatUint(sv.Major(), 10),
Minor: strconv.FormatUint(sv.Minor(), 10),
}, nil
}
// VersionSet is a set of Kubernetes API versions.
type VersionSet []string

View File

@@ -147,12 +147,15 @@ service:
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: chart-example.local
paths: []
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
@@ -212,7 +215,14 @@ const defaultIgnore = `# Patterns to ignore when building packages.
const defaultIngress = `{{- if .Values.ingress.enabled -}}
{{- $fullName := include "<CHARTNAME>.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
@@ -227,6 +237,9 @@ metadata:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
@@ -244,12 +257,22 @@ spec:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
`
const defaultDeployment = `apiVersion: apps/v1

View File

@@ -61,7 +61,7 @@ const (
// ValidateReleaseName performs checks for an entry for a Helm release name
//
// For Helm to allow a name, it must be below a certain character count (53) and also match
// a reguar expression.
// a regular expression.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
@@ -96,6 +96,9 @@ func ValidateReleaseName(name string) error {
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// Deprecated: remove in Helm 4. Name validation now uses rules defined in
// pkg/lint/rules.validateMetadataNameFunc()
func ValidateMetadataName(name string) error {
if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
return errInvalidKubernetesName

View File

@@ -0,0 +1,384 @@
/*
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"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/internal/fileutil"
"helm.sh/helm/v3/internal/urlutil"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/provenance"
"helm.sh/helm/v3/pkg/repo"
)
// 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
// Getter collection for the operation
Getters getter.Providers
// Options provide parameters to be passed along to the Getter being initialized.
Options []getter.Option
RegistryClient *registry.Client
RepositoryConfig string
RepositoryCache 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, err := c.ResolveChartVersion(ref, version)
if err != nil {
return "", nil, err
}
g, err := c.Getters.ByScheme(u.Scheme)
if err != nil {
return "", nil, err
}
data, err := g.Get(u.String(), c.Options...)
if err != nil {
return "", nil, err
}
name := filepath.Base(u.Path)
if u.Scheme == "oci" {
name = fmt.Sprintf("%s-%s.tgz", name, version)
}
destfile := filepath.Join(dest, name)
if err := fileutil.AtomicWriteFile(destfile, data, 0644); err != nil {
return destfile, nil, err
}
// If provenance is requested, verify it.
ver := &provenance.Verification{}
if c.Verify > VerifyNever {
body, err := g.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 := fileutil.AtomicWriteFile(provfile, body, 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 and sets the ChartDownloader's Options that can fetch
// the URL using the appropriate Getter.
//
// 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, error) {
u, err := url.Parse(ref)
if err != nil {
return nil, errors.Errorf("invalid chart URL format: %s", ref)
}
rf, err := loadRepoConfig(c.RepositoryConfig)
if err != nil {
return u, 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 {
// Make sure to add the ref URL as the URL for the getter
c.Options = append(c.Options, getter.WithURL(ref))
return u, nil
}
return u, err
}
// 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 set the parameters and return.
c.Options = append(
c.Options,
getter.WithURL(rc.URL),
)
if rc.CertFile != "" || rc.KeyFile != "" || rc.CAFile != "" {
c.Options = append(c.Options, getter.WithTLSClientConfig(rc.CertFile, rc.KeyFile, rc.CAFile))
}
if rc.Username != "" && rc.Password != "" {
c.Options = append(
c.Options,
getter.WithBasicAuth(rc.Username, rc.Password),
getter.WithPassCredentialsAll(rc.PassCredentialsAll),
)
}
return u, nil
}
// See if it's of the form: repo/path_to_chart
p := strings.SplitN(u.Path, "/", 2)
if len(p) < 2 {
return u, 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, err
}
// Now that we have the chart repository information we can use that URL
// to set the URL for the getter.
c.Options = append(c.Options, getter.WithURL(rc.URL))
r, err := repo.NewChartRepository(rc, c.Getters)
if err != nil {
return u, err
}
if r != nil && r.Config != nil {
if r.Config.CertFile != "" || r.Config.KeyFile != "" || r.Config.CAFile != "" {
c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile))
}
if r.Config.Username != "" && r.Config.Password != "" {
c.Options = append(c.Options,
getter.WithBasicAuth(r.Config.Username, r.Config.Password),
getter.WithPassCredentialsAll(r.Config.PassCredentialsAll),
)
}
}
// Next, we need to load the index, and actually look up the chart.
idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name))
i, err := repo.LoadIndexFile(idxFile)
if err != nil {
return u, errors.Wrap(err, "no cached repo found. (try 'helm repo update')")
}
cv, err := i.Get(chartName, version)
if err != nil {
return u, 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, 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, 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, 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
if _, err := getter.NewHTTPGetter(getter.WithURL(rc.URL)); err != nil {
return repoURL, err
}
return u, err
}
// TODO add user-agent
return u, nil
}
// 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.
switch fi, err := os.Stat(path); {
case err != nil:
return nil, err
case fi.IsDir():
return nil, errors.New("unpacked charts cannot be verified")
case !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.EqualFold(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
}
idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name))
i, err := repo.LoadIndexFile(idxFile)
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
}
func loadRepoConfig(file string) (*repo.File, error) {
r, err := repo.LoadFile(file)
if err != nil && !os.IsNotExist(errors.Cause(err)) {
return nil, err
}
return r, nil
}

View File

@@ -0,0 +1,23 @@
/*
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 provides a library for downloading charts.
This package contains various tools for downloading charts from repository
servers, and then storing them in Helm-specific directory structures. This
library contains many functions that depend on a specific
filesystem layout.
*/
package downloader

View File

@@ -0,0 +1,898 @@
/*
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 (
"crypto"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/Masterminds/semver/v3"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/internal/resolver"
"helm.sh/helm/v3/internal/third_party/dep/fs"
"helm.sh/helm/v3/internal/urlutil"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/repo"
)
// ErrRepoNotFound indicates that chart repositories can't be found in local repo cache.
// The value of Repos is missing repos.
type ErrRepoNotFound struct {
Repos []string
}
// Error implements the error interface.
func (e ErrRepoNotFound) Error() string {
return fmt.Sprintf("no repository definition for %s", strings.Join(e.Repos, ", "))
}
// Manager handles the lifecycle of fetching, resolving, and storing dependencies.
type Manager struct {
// Out is used to print warnings and notifications.
Out io.Writer
// ChartPath is the path to the unpacked base chart upon which this operates.
ChartPath string
// 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
RegistryClient *registry.Client
RepositoryConfig string
RepositoryCache string
}
// Build rebuilds a local charts directory from a lockfile.
//
// If the lockfile is not present, this will run a Manager.Update()
//
// If SkipUpdate is set, this will not update the repository.
func (m *Manager) Build() error {
c, err := m.loadChartDir()
if err != nil {
return err
}
// If a lock file is found, run a build from that. Otherwise, just do
// an update.
lock := c.Lock
if lock == nil {
return m.Update()
}
// Check that all of the repos we're dependent on actually exist.
req := c.Metadata.Dependencies
// If using apiVersion v1, calculate the hash before resolve repo names
// because resolveRepoNames will change req if req uses repo alias
// and Helm 2 calculate the digest from the original req
// Fix for: https://github.com/helm/helm/issues/7619
var v2Sum string
if c.Metadata.APIVersion == chart.APIVersionV1 {
v2Sum, err = resolver.HashV2Req(req)
if err != nil {
return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies")
}
}
if _, err := m.resolveRepoNames(req); err != nil {
return err
}
if sum, err := resolver.HashReq(req, lock.Dependencies); err != nil || sum != lock.Digest {
// If lock digest differs and chart is apiVersion v1, it maybe because the lock was built
// with Helm 2 and therefore should be checked with Helm v2 hash
// Fix for: https://github.com/helm/helm/issues/7233
if c.Metadata.APIVersion == chart.APIVersionV1 {
log.Println("warning: a valid Helm v3 hash was not found. Checking against Helm v2 hash...")
if v2Sum != lock.Digest {
return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies")
}
} else {
return errors.New("the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies")
}
}
// 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/
return m.downloadAll(lock.Dependencies)
}
// Update updates a local charts directory.
//
// It first reads the Chart.yaml file, and then attempts to
// negotiate versions based on that. It will download the versions
// from remote chart repositories unless SkipUpdate is true.
func (m *Manager) Update() error {
c, err := m.loadChartDir()
if err != nil {
return err
}
// If no dependencies are found, we consider this a successful
// completion.
req := c.Metadata.Dependencies
if req == nil {
return nil
}
// Get the names of the repositories the dependencies need that Helm is
// configured to know about.
repoNames, err := m.resolveRepoNames(req)
if err != nil {
return err
}
// For the repositories Helm is not configured to know about, ensure Helm
// has some information about them and, when possible, the index files
// locally.
// TODO(mattfarina): Repositories should be explicitly added by end users
// rather than automattic. In Helm v4 require users to add repositories. They
// should have to add them in order to make sure they are aware of the
// repositories and opt-in to any locations, for security.
repoNames, err = m.ensureMissingRepos(repoNames, req)
if err != nil {
return err
}
// For each of the repositories Helm is configured to know about, update
// the index information locally.
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)
if err != nil {
return err
}
// Now we need to fetch every package here into charts/
if err := m.downloadAll(lock.Dependencies); err != nil {
return err
}
// downloadAll might overwrite dependency version, recalculate lock digest
newDigest, err := resolver.HashReq(req, lock.Dependencies)
if err != nil {
return err
}
lock.Digest = newDigest
// If the lock file hasn't changed, don't write a new one.
oldLock := c.Lock
if oldLock != nil && oldLock.Digest == lock.Digest {
return nil
}
// Finally, we need to write the lockfile.
return writeLock(m.ChartPath, lock, c.Metadata.APIVersion == chart.APIVersionV1)
}
func (m *Manager) loadChartDir() (*chart.Chart, error) {
if fi, err := os.Stat(m.ChartPath); err != nil {
return nil, errors.Wrapf(err, "could not find %s", m.ChartPath)
} else if !fi.IsDir() {
return nil, errors.New("only unpacked charts can be updated")
}
return loader.LoadDir(m.ChartPath)
}
// 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) (*chart.Lock, error) {
res := resolver.New(m.ChartPath, m.RepositoryCache)
return res.Resolve(req, repoNames)
}
// downloadAll takes a list of dependencies and downloads them into charts/
//
// 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")
// Create 'charts' directory if it doesn't already exist.
if fi, err := os.Stat(destPath); err != nil {
if err := os.MkdirAll(destPath, 0755); err != nil {
return err
}
} else if !fi.IsDir() {
return errors.Errorf("%q is not a directory", destPath)
}
if err := fs.RenameWithFallback(destPath, tmpPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
}
if err := os.MkdirAll(destPath, 0755); err != nil {
return err
}
fmt.Fprintf(m.Out, "Saving %d charts\n", len(deps))
var saveError error
churls := make(map[string]struct{})
for _, dep := range deps {
// No repository means the chart is in charts directory
if dep.Repository == "" {
fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name)
chartPath := filepath.Join(tmpPath, dep.Name)
ch, err := loader.LoadDir(chartPath)
if err != nil {
return fmt.Errorf("Unable to load chart: %v", err)
}
constraint, err := semver.NewConstraint(dep.Version)
if err != nil {
return fmt.Errorf("Dependency %s has an invalid version/constraint format: %s", dep.Name, err)
}
v, err := semver.NewVersion(ch.Metadata.Version)
if err != nil {
return fmt.Errorf("Invalid version %s for dependency %s: %s", dep.Version, dep.Name, err)
}
if !constraint.Check(v) {
saveError = fmt.Errorf("Dependency %s at version %s does not satisfy the constraint %s", dep.Name, ch.Metadata.Version, dep.Version)
break
}
continue
}
if strings.HasPrefix(dep.Repository, "file://") {
if m.Debug {
fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository)
}
ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version)
if err != nil {
saveError = err
break
}
dep.Version = ver
continue
}
// Any failure to resolve/download a chart should fail:
// https://github.com/helm/helm/issues/1439
churl, username, password, insecureskiptlsverify, passcredentialsall, caFile, certFile, keyFile, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos)
if err != nil {
saveError = errors.Wrapf(err, "could not find %s", churl)
break
}
if _, ok := churls[churl]; ok {
fmt.Fprintf(m.Out, "Already downloaded %s from repo %s\n", dep.Name, dep.Repository)
continue
}
fmt.Fprintf(m.Out, "Downloading %s from repo %s\n", dep.Name, dep.Repository)
dl := ChartDownloader{
Out: m.Out,
Verify: m.Verify,
Keyring: m.Keyring,
RepositoryConfig: m.RepositoryConfig,
RepositoryCache: m.RepositoryCache,
Getters: m.Getters,
Options: []getter.Option{
getter.WithBasicAuth(username, password),
getter.WithPassCredentialsAll(passcredentialsall),
getter.WithInsecureSkipVerifyTLS(insecureskiptlsverify),
getter.WithTLSClientConfig(certFile, keyFile, caFile),
},
}
version := ""
if strings.HasPrefix(churl, "oci://") {
if !resolver.FeatureGateOCI.IsEnabled() {
return errors.Wrapf(resolver.FeatureGateOCI.Error(),
"the repository %s is an OCI registry", churl)
}
churl, version, err = parseOCIRef(churl)
if err != nil {
return errors.Wrapf(err, "could not parse OCI reference")
}
dl.Options = append(dl.Options,
getter.WithRegistryClient(m.RegistryClient),
getter.WithTagName(version))
}
_, _, err = dl.DownloadTo(churl, version, destPath)
if err != nil {
saveError = errors.Wrapf(err, "could not download %s", churl)
break
}
churls[churl] = struct{}{}
}
if saveError == nil {
fmt.Fprintln(m.Out, "Deleting outdated charts")
for _, dep := range deps {
// Chart from local charts directory stays in place
if dep.Repository != "" {
if err := m.safeDeleteDep(dep.Name, tmpPath); err != nil {
return err
}
}
}
if err := move(tmpPath, destPath); err != nil {
return err
}
if err := os.RemoveAll(tmpPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", tmpPath)
}
} else {
fmt.Fprintln(m.Out, "Save error occurred: ", saveError)
fmt.Fprintln(m.Out, "Deleting newly downloaded charts, restoring pre-update state")
for _, dep := range deps {
if err := m.safeDeleteDep(dep.Name, destPath); err != nil {
return err
}
}
if err := os.RemoveAll(destPath); err != nil {
return errors.Wrapf(err, "failed to remove %v", destPath)
}
if err := fs.RenameWithFallback(tmpPath, destPath); err != nil {
return errors.Wrap(err, "unable to move current charts to tmp dir")
}
return saveError
}
return nil
}
func parseOCIRef(chartRef string) (string, string, error) {
refTagRegexp := regexp.MustCompile(`^(oci://[^:]+(:[0-9]{1,5})?[^:]+):(.*)$`)
caps := refTagRegexp.FindStringSubmatch(chartRef)
if len(caps) != 4 {
return "", "", errors.Errorf("improperly formatted oci chart reference: %s", chartRef)
}
chartRef = caps[1]
tag := caps[3]
return chartRef, tag, nil
}
// safeDeleteDep deletes any versions of the given dependency in the given directory.
//
// It does this by first matching the file name to an expected pattern, then loading
// the file to verify that it is a chart with the same name as the given name.
//
// Because it requires tar file introspection, it is more intensive than a basic delete.
//
// This will only return errors that should stop processing entirely. Other errors
// will emit log messages or be ignored.
func (m *Manager) safeDeleteDep(name, dir string) error {
files, err := filepath.Glob(filepath.Join(dir, name+"-*.tgz"))
if err != nil {
// Only for ErrBadPattern
return err
}
for _, fname := range files {
ch, err := loader.LoadFile(fname)
if err != nil {
fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)", fname, err)
continue
}
if ch.Name() != name {
// This is not the file you are looking for.
continue
}
if err := os.Remove(fname); err != nil {
fmt.Fprintf(m.Out, "Could not delete %s: %s (Skipping)", fname, err)
continue
}
}
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 := loadRepoConfig(m.RepositoryConfig)
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 or OCI, continue
if strings.HasPrefix(dd.Repository, "file://") || strings.HasPrefix(dd.Repository, "oci://") {
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 ErrRepoNotFound{missing}
}
return nil
}
// ensureMissingRepos attempts to ensure the repository information for repos
// not managed by Helm is present. This takes in the repoNames Helm is configured
// to work with along with the chart dependencies. It will find the deps not
// in a known repo and attempt to ensure the data is present for steps like
// version resolution.
func (m *Manager) ensureMissingRepos(repoNames map[string]string, deps []*chart.Dependency) (map[string]string, error) {
var ru []*repo.Entry
for _, dd := range deps {
// If the chart is in the local charts directory no repository needs
// to be specified.
if dd.Repository == "" {
continue
}
// When the repoName for a dependency is known we can skip ensuring
if _, ok := repoNames[dd.Name]; ok {
continue
}
// The generated repository name, which will result in an index being
// locally cached, has a name pattern of "helm-manager-" followed by a
// sha256 of the repo name. This assumes end users will never create
// repositories with these names pointing to other repositories. Using
// this method of naming allows the existing repository pulling and
// resolution code to do most of the work.
rn, err := key(dd.Repository)
if err != nil {
return repoNames, err
}
rn = managerKeyPrefix + rn
repoNames[dd.Name] = rn
// Assuming the repository is generally available. For Helm managed
// access controls the repository needs to be added through the user
// managed system. This path will work for public charts, like those
// supplied by Bitnami, but not for protected charts, like corp ones
// behind a username and pass.
ri := &repo.Entry{
Name: rn,
URL: dd.Repository,
}
ru = append(ru, ri)
}
// Calls to UpdateRepositories (a public function) will only update
// repositories configured by the user. Here we update repos found in
// the dependencies that are not known to the user if update skipping
// is not configured.
if !m.SkipUpdate && len(ru) > 0 {
fmt.Fprintln(m.Out, "Getting updates for unmanaged Helm repositories...")
if err := m.parallelRepoUpdate(ru); err != nil {
return repoNames, err
}
}
return repoNames, nil
}
// resolveRepoNames returns the repo names of the referenced deps which can be used to fetch the cached index file
// and replaces aliased repository URLs into resolved URLs in dependencies.
func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, error) {
rf, err := loadRepoConfig(m.RepositoryConfig)
if err != nil {
if os.IsNotExist(err) {
return make(map[string]string), 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 {
// Don't map the repository, we don't need to download chart from charts directory
// When OCI is used there is no Helm repository
if dd.Repository == "" || strings.HasPrefix(dd.Repository, "oci://") {
continue
}
// 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
}
if strings.HasPrefix(dd.Repository, "oci://") {
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 {
repository := dd.Repository
// Add if URL
_, err := url.ParseRequestURI(repository)
if err == nil {
reposMap[repository] = repository
continue
}
missing = append(missing, 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 "example"
repository, use "https://charts.example.com/" or "@example" instead of
"example". 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 := loadRepoConfig(m.RepositoryConfig)
if err != nil {
return err
}
repos := rf.Repositories
if len(repos) > 0 {
fmt.Fprintln(m.Out, "Hang tight while we grab the latest from your chart repositories...")
// This prints warnings straight to out.
if err := m.parallelRepoUpdate(repos); err != nil {
return err
}
fmt.Fprintln(m.Out, "Update Complete. ⎈Happy Helming!⎈")
}
return nil
}
func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
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(); err != nil {
// For those dependencies that are not known to helm and using a
// generated key name we display the repo url.
if strings.HasPrefix(r.Config.Name, managerKeyPrefix) {
fmt.Fprintf(m.Out, "...Unable to get an update from the %q chart repository:\n\t%s\n", r.Config.URL, err)
} else {
fmt.Fprintf(m.Out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", r.Config.Name, r.Config.URL, err)
}
} else {
// For those dependencies that are not known to helm and using a
// generated key name we display the repo url.
if strings.HasPrefix(r.Config.Name, managerKeyPrefix) {
fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.URL)
} else {
fmt.Fprintf(m.Out, "...Successfully got an update from the %q chart repository\n", r.Config.Name)
}
}
wg.Done()
}(r)
}
wg.Wait()
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 (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureskiptlsverify, passcredentialsall bool, caFile, certFile, keyFile string, err error) {
if strings.HasPrefix(repoURL, "oci://") {
return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, "", "", "", nil
}
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
passcredentialsall = cr.Config.PassCredentialsAll
insecureskiptlsverify = cr.Config.InsecureSkipTLSverify
caFile = cr.Config.CAFile
certFile = cr.Config.CertFile
keyFile = cr.Config.KeyFile
return
}
}
url, err = repo.FindChartInRepoURL(repoURL, name, version, certFile, keyFile, caFile, m.Getters)
if err == nil {
return url, username, password, false, false, "", "", "", err
}
err = errors.Errorf("chart %s not found in %s: %s", name, repoURL, err)
return url, username, password, false, false, "", "", "", err
}
// 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 {
// Fallback to string comparison.
return v1 == v2
}
sv2, err := semver.NewVersion(v2)
if err != nil {
return false
}
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{}
// Load repositories.yaml file
rf, err := loadRepoConfig(m.RepositoryConfig)
if err != nil {
return indices, errors.Wrapf(err, "failed to load %s", m.RepositoryConfig)
}
for _, re := range rf.Repositories {
lname := re.Name
idxFile := filepath.Join(m.RepositoryCache, helmpath.CacheIndexFile(lname))
index, err := repo.LoadIndexFile(idxFile)
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, legacyLockfile bool) error {
data, err := yaml.Marshal(lock)
if err != nil {
return err
}
lockfileName := "Chart.lock"
if legacyLockfile {
lockfileName = "requirements.lock"
}
dest := filepath.Join(chartpath, lockfileName)
return ioutil.WriteFile(dest, data, 0644)
}
// archive a dep chart from local directory and save it into charts/
func tarFromLocalDir(chartpath, name, repo, 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)
}
origPath, err := resolver.GetLocalPath(repo, chartpath)
if err != nil {
return "", err
}
ch, err := loader.LoadDir(origPath)
if err != nil {
return "", err
}
constraint, err := semver.NewConstraint(version)
if err != nil {
return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name)
}
v, err := semver.NewVersion(ch.Metadata.Version)
if err != nil {
return "", err
}
if constraint.Check(v) {
_, err = chartutil.Save(ch, destPath)
return ch.Metadata.Version, err
}
return "", errors.Errorf("can't get a valid version for dependency %s", name)
}
// move files from tmppath to destpath
func move(tmpPath, destPath string) error {
files, _ := ioutil.ReadDir(tmpPath)
for _, file := range files {
filename := file.Name()
tmpfile := filepath.Join(tmpPath, filename)
destfile := filepath.Join(destPath, filename)
if err := fs.RenameWithFallback(tmpfile, destfile); err != nil {
return errors.Wrap(err, "unable to move local charts to charts dir")
}
}
return nil
}
// The prefix to use for cache keys created by the manager for repo names
const managerKeyPrefix = "helm-manager-"
// key is used to turn a name, such as a repository url, into a filesystem
// safe name that is unique for querying. To accomplish this a unique hash of
// the string is used.
func key(name string) (string, error) {
in := strings.NewReader(name)
hash := crypto.SHA256.New()
if _, err := io.Copy(hash, in); err != nil {
return "", nil
}
return hex.EncodeToString(hash.Sum(nil)), nil
}

View File

@@ -0,0 +1,23 @@
/*
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 engine implements the Go text template engine as needed for Helm.
When Helm renders templates it does so with additional functions and different
modes (e.g., strict, lint mode). This package handles the helm specific
implementation.
*/
package engine // import "helm.sh/helm/v3/pkg/engine"

View File

@@ -0,0 +1,392 @@
/*
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 engine
import (
"fmt"
"log"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"text/template"
"github.com/pkg/errors"
"k8s.io/client-go/rest"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
)
// Engine is an implementation of the Helm rendering implementation for templates.
type Engine struct {
// If strict is enabled, template rendering will fail if a template references
// a value that was not passed in.
Strict bool
// In LintMode, some 'required' template values may be missing, so don't fail
LintMode bool
// the rest config to connect to the kubernetes api
config *rest.Config
}
// Render takes a chart, optional values, and value overrides, and attempts to render the Go templates.
//
// Render can be called repeatedly on the same engine.
//
// This will look in the chart's 'templates' data (e.g. the 'templates/' directory)
// and attempt to render the templates there using the values passed in.
//
// Values are scoped to their templates. A dependency template will not have
// access to the values set for its parent. If chart "foo" includes chart "bar",
// "bar" will not have access to the values for "foo".
//
// Values should be prepared with something like `chartutils.ReadValues`.
//
// Values are passed through the templates according to scope. If the top layer
// chart includes the chart foo, which includes the chart bar, the values map
// will be examined for a table called "foo". If "foo" is found in vals,
// that section of the values will be passed into the "foo" chart. And if that
// section contains a value named "bar", that value will be passed on to the
// bar chart during render time.
func (e Engine) Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
tmap := allTemplates(chrt, values)
return e.render(tmap)
}
// Render takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options.
func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, error) {
return new(Engine).Render(chrt, values)
}
// RenderWithClient takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client
func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) {
return Engine{
config: config,
}.Render(chrt, values)
}
// renderable is an object that can be rendered.
type renderable struct {
// tpl is the current template.
tpl string
// vals are the values to be supplied to the template.
vals chartutil.Values
// namespace prefix to the templates of the current chart
basePath string
}
const warnStartDelim = "HELM_ERR_START"
const warnEndDelim = "HELM_ERR_END"
const recursionMaxNums = 1000
var warnRegex = regexp.MustCompile(warnStartDelim + `(.*)` + warnEndDelim)
func warnWrap(warn string) string {
return warnStartDelim + warn + warnEndDelim
}
// initFunMap creates the Engine's FuncMap and adds context-specific functions.
func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]renderable) {
funcMap := funcMap()
includedNames := make(map[string]int)
// Add the 'include' function here so we can close over t.
funcMap["include"] = func(name string, data interface{}) (string, error) {
var buf strings.Builder
if v, ok := includedNames[name]; ok {
if v > recursionMaxNums {
return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name)
}
includedNames[name]++
} else {
includedNames[name] = 1
}
err := t.ExecuteTemplate(&buf, name, data)
includedNames[name]--
return buf.String(), err
}
// Add the 'tpl' function here
funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) {
basePath, err := vals.PathValue("Template.BasePath")
if err != nil {
return "", errors.Wrapf(err, "cannot retrieve Template.Basepath from values inside tpl function: %s", tpl)
}
templateName, err := vals.PathValue("Template.Name")
if err != nil {
return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl)
}
templates := map[string]renderable{
templateName.(string): {
tpl: tpl,
vals: vals,
basePath: basePath.(string),
},
}
result, err := e.renderWithReferences(templates, referenceTpls)
if err != nil {
return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl)
}
return result[templateName.(string)], nil
}
// Add the `required` function here so we can use lintMode
funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
if val == nil {
if e.LintMode {
// Don't fail on missing required values when linting
log.Printf("[INFO] Missing required value: %s", warn)
return "", nil
}
return val, errors.Errorf(warnWrap(warn))
} else if _, ok := val.(string); ok {
if val == "" {
if e.LintMode {
// Don't fail on missing required values when linting
log.Printf("[INFO] Missing required value: %s", warn)
return "", nil
}
return val, errors.Errorf(warnWrap(warn))
}
}
return val, nil
}
// Override sprig fail function for linting and wrapping message
funcMap["fail"] = func(msg string) (string, error) {
if e.LintMode {
// Don't fail when linting
log.Printf("[INFO] Fail: %s", msg)
return "", nil
}
return "", errors.New(warnWrap(msg))
}
// If we are not linting and have a cluster connection, provide a Kubernetes-backed
// implementation.
if !e.LintMode && e.config != nil {
funcMap["lookup"] = NewLookupFunction(e.config)
}
t.Funcs(funcMap)
}
// render takes a map of templates/values and renders them.
func (e Engine) render(tpls map[string]renderable) (map[string]string, error) {
return e.renderWithReferences(tpls, tpls)
}
// renderWithReferences takes a map of templates/values to render, and a map of
// templates which can be referenced within them.
func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) (rendered map[string]string, err error) {
// Basically, what we do here is start with an empty parent template and then
// build up a list of templates -- one for each file. Once all of the templates
// have been parsed, we loop through again and execute every template.
//
// The idea with this process is to make it possible for more complex templates
// to share common blocks, but to make the entire thing feel like a file-based
// template engine.
defer func() {
if r := recover(); r != nil {
err = errors.Errorf("rendering template failed: %v", r)
}
}()
t := template.New("gotpl")
if e.Strict {
t.Option("missingkey=error")
} else {
// Not that zero will attempt to add default values for types it knows,
// but will still emit <no value> for others. We mitigate that later.
t.Option("missingkey=zero")
}
e.initFunMap(t, referenceTpls)
// We want to parse the templates in a predictable order. The order favors
// higher-level (in file system) templates over deeply nested templates.
keys := sortTemplates(tpls)
referenceKeys := sortTemplates(referenceTpls)
for _, filename := range keys {
r := tpls[filename]
if _, err := t.New(filename).Parse(r.tpl); err != nil {
return map[string]string{}, cleanupParseError(filename, err)
}
}
// Adding the reference templates to the template context
// so they can be referenced in the tpl function
for _, filename := range referenceKeys {
if t.Lookup(filename) == nil {
r := referenceTpls[filename]
if _, err := t.New(filename).Parse(r.tpl); err != nil {
return map[string]string{}, cleanupParseError(filename, err)
}
}
}
rendered = make(map[string]string, len(keys))
for _, filename := range keys {
// Don't render partials. We don't care out the direct output of partials.
// They are only included from other templates.
if strings.HasPrefix(path.Base(filename), "_") {
continue
}
// At render time, add information about the template that is being rendered.
vals := tpls[filename].vals
vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath}
var buf strings.Builder
if err := t.ExecuteTemplate(&buf, filename, vals); err != nil {
return map[string]string{}, cleanupExecError(filename, err)
}
// Work around the issue where Go will emit "<no value>" even if Options(missing=zero)
// is set. Since missing=error will never get here, we do not need to handle
// the Strict case.
rendered[filename] = strings.ReplaceAll(buf.String(), "<no value>", "")
}
return rendered, nil
}
func cleanupParseError(filename string, err error) error {
tokens := strings.Split(err.Error(), ": ")
if len(tokens) == 1 {
// This might happen if a non-templating error occurs
return fmt.Errorf("parse error in (%s): %s", filename, err)
}
// The first token is "template"
// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
location := tokens[1]
// The remaining tokens make up a stacktrace-like chain, ending with the relevant error
errMsg := tokens[len(tokens)-1]
return fmt.Errorf("parse error at (%s): %s", string(location), errMsg)
}
func cleanupExecError(filename string, err error) error {
if _, isExecError := err.(template.ExecError); !isExecError {
return err
}
tokens := strings.SplitN(err.Error(), ": ", 3)
if len(tokens) != 3 {
// This might happen if a non-templating error occurs
return fmt.Errorf("execution error in (%s): %s", filename, err)
}
// The first token is "template"
// The second token is either "filename:lineno" or "filename:lineNo:columnNo"
location := tokens[1]
parts := warnRegex.FindStringSubmatch(tokens[2])
if len(parts) >= 2 {
return fmt.Errorf("execution error at (%s): %s", string(location), parts[1])
}
return err
}
func sortTemplates(tpls map[string]renderable) []string {
keys := make([]string, len(tpls))
i := 0
for key := range tpls {
keys[i] = key
i++
}
sort.Sort(sort.Reverse(byPathLen(keys)))
return keys
}
type byPathLen []string
func (p byPathLen) Len() int { return len(p) }
func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] }
func (p byPathLen) Less(i, j int) bool {
a, b := p[i], p[j]
ca, cb := strings.Count(a, "/"), strings.Count(b, "/")
if ca == cb {
return strings.Compare(a, b) == -1
}
return ca < cb
}
// allTemplates returns all templates for a chart and its dependencies.
//
// As it goes, it also prepares the values in a scope-sensitive manner.
func allTemplates(c *chart.Chart, vals chartutil.Values) map[string]renderable {
templates := make(map[string]renderable)
recAllTpls(c, templates, vals)
return templates
}
// recAllTpls recurses through the templates in a chart.
//
// As it recurses, it also sets the values to be appropriate for the template
// scope.
func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values) {
next := map[string]interface{}{
"Chart": c.Metadata,
"Files": newFiles(c.Files),
"Release": vals["Release"],
"Capabilities": vals["Capabilities"],
"Values": make(chartutil.Values),
}
// If there is a {{.Values.ThisChart}} in the parent metadata,
// copy that into the {{.Values}} for this template.
if c.IsRoot() {
next["Values"] = vals["Values"]
} else if vs, err := vals.Table("Values." + c.Name()); err == nil {
next["Values"] = vs
}
for _, child := range c.Dependencies() {
recAllTpls(child, templates, next)
}
newParentID := c.ChartFullPath()
for _, t := range c.Templates {
if !isTemplateValid(c, t.Name) {
continue
}
templates[path.Join(newParentID, t.Name)] = renderable{
tpl: string(t.Data),
vals: next,
basePath: path.Join(newParentID, "templates"),
}
}
}
// isTemplateValid returns true if the template is valid for the chart type
func isTemplateValid(ch *chart.Chart, templateName string) bool {
if isLibraryChart(ch) {
return strings.HasPrefix(filepath.Base(templateName), "_")
}
return true
}
// isLibraryChart returns true if the chart is a library chart
func isLibraryChart(c *chart.Chart) bool {
return strings.EqualFold(c.Metadata.Type, "library")
}

View File

@@ -0,0 +1,160 @@
/*
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 engine
import (
"encoding/base64"
"path"
"strings"
"github.com/gobwas/glob"
"helm.sh/helm/v3/pkg/chart"
)
// files is a map of files in a chart that can be accessed from a template.
type files map[string][]byte
// NewFiles creates a new files from chart files.
// Given an []*chart.File (the format for files in a chart.Chart), extract a map of files.
func newFiles(from []*chart.File) files {
files := make(map[string][]byte)
for _, f := range from {
files[f.Name] = f.Data
}
return files
}
// GetBytes gets a file by path.
//
// The returned data is raw. In a template context, this is identical to calling
// {{index .Files $path}}.
//
// This is intended to be accessed from within a template, so a missed key returns
// an empty []byte.
func (f files) GetBytes(name string) []byte {
if v, ok := f[name]; ok {
return v
}
return []byte{}
}
// Get returns a string representation of the given file.
//
// Fetch the contents of a file as a string. It is designed to be called in a
// template.
//
// {{.Files.Get "foo"}}
func (f files) Get(name string) string {
return string(f.GetBytes(name))
}
// Glob takes a glob pattern and returns another files object only containing
// matched files.
//
// This is designed to be called from a template.
//
// {{ range $name, $content := .Files.Glob("foo/**") }}
// {{ $name }}: |
// {{ .Files.Get($name) | indent 4 }}{{ end }}
func (f files) Glob(pattern string) files {
g, err := glob.Compile(pattern, '/')
if err != nil {
g, _ = glob.Compile("**")
}
nf := newFiles(nil)
for name, contents := range f {
if g.Match(name) {
nf[name] = contents
}
}
return nf
}
// AsConfig turns a Files group and flattens it to a YAML map suitable for
// including in the 'data' section of a Kubernetes ConfigMap definition.
// Duplicate keys will be overwritten, so be aware that your file names
// (regardless of path) should be unique.
//
// This is designed to be called from a template, and will return empty string
// (via toYAML function) if it cannot be serialized to YAML, or if the Files
// object is nil.
//
// The output will not be indented, so you will want to pipe this to the
// 'indent' template function.
//
// data:
// {{ .Files.Glob("config/**").AsConfig() | indent 4 }}
func (f files) AsConfig() string {
if f == nil {
return ""
}
m := make(map[string]string)
// Explicitly convert to strings, and file names
for k, v := range f {
m[path.Base(k)] = string(v)
}
return toYAML(m)
}
// AsSecrets returns the base64-encoded value of a Files object suitable for
// including in the 'data' section of a Kubernetes Secret definition.
// Duplicate keys will be overwritten, so be aware that your file names
// (regardless of path) should be unique.
//
// This is designed to be called from a template, and will return empty string
// (via toYAML function) if it cannot be serialized to YAML, or if the Files
// object is nil.
//
// The output will not be indented, so you will want to pipe this to the
// 'indent' template function.
//
// data:
// {{ .Files.Glob("secrets/*").AsSecrets() }}
func (f files) AsSecrets() string {
if f == nil {
return ""
}
m := make(map[string]string)
for k, v := range f {
m[path.Base(k)] = base64.StdEncoding.EncodeToString(v)
}
return toYAML(m)
}
// Lines returns each line of a named file (split by "\n") as a slice, so it can
// be ranged over in your templates.
//
// This is designed to be called from a template.
//
// {{ range .Files.Lines "foo/bar.html" }}
// {{ . }}{{ end }}
func (f files) Lines(path string) []string {
if f == nil || f[path] == nil {
return []string{}
}
return strings.Split(string(f[path]), "\n")
}

View File

@@ -0,0 +1,177 @@
/*
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 engine
import (
"bytes"
"encoding/json"
"strings"
"text/template"
"github.com/BurntSushi/toml"
"github.com/Masterminds/sprig/v3"
"sigs.k8s.io/yaml"
)
// funcMap returns a mapping of all of the functions that Engine has.
//
// Because some functions are late-bound (e.g. contain context-sensitive
// data), the functions may not all perform identically outside of an Engine
// as they will inside of an Engine.
//
// Known late-bound functions:
//
// - "include"
// - "tpl"
//
// These are late-bound in Engine.Render(). The
// version included in the FuncMap is a placeholder.
//
func funcMap() template.FuncMap {
f := sprig.TxtFuncMap()
delete(f, "env")
delete(f, "expandenv")
// Add some extra functionality
extra := template.FuncMap{
"toToml": toTOML,
"toYaml": toYAML,
"fromYaml": fromYAML,
"fromYamlArray": fromYAMLArray,
"toJson": toJSON,
"fromJson": fromJSON,
"fromJsonArray": fromJSONArray,
// This is a placeholder for the "include" function, which is
// late-bound to a template. By declaring it here, we preserve the
// integrity of the linter.
"include": func(string, interface{}) string { return "not implemented" },
"tpl": func(string, interface{}) interface{} { return "not implemented" },
"required": func(string, interface{}) (interface{}, error) { return "not implemented", nil },
// Provide a placeholder for the "lookup" function, which requires a kubernetes
// connection.
"lookup": func(string, string, string, string) (map[string]interface{}, error) {
return map[string]interface{}{}, nil
},
}
for k, v := range extra {
f[k] = v
}
return f
}
// toYAML takes an interface, marshals it to yaml, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func toYAML(v interface{}) string {
data, err := yaml.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return ""
}
return strings.TrimSuffix(string(data), "\n")
}
// fromYAML converts a YAML document into a map[string]interface{}.
//
// This is not a general-purpose YAML parser, and will not parse all valid
// YAML documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string into
// m["Error"] in the returned map.
func fromYAML(str string) map[string]interface{} {
m := map[string]interface{}{}
if err := yaml.Unmarshal([]byte(str), &m); err != nil {
m["Error"] = err.Error()
}
return m
}
// fromYAMLArray converts a YAML array into a []interface{}.
//
// This is not a general-purpose YAML parser, and will not parse all valid
// YAML documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string as
// the first and only item in the returned array.
func fromYAMLArray(str string) []interface{} {
a := []interface{}{}
if err := yaml.Unmarshal([]byte(str), &a); err != nil {
a = []interface{}{err.Error()}
}
return a
}
// toTOML takes an interface, marshals it to toml, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func toTOML(v interface{}) string {
b := bytes.NewBuffer(nil)
e := toml.NewEncoder(b)
err := e.Encode(v)
if err != nil {
return err.Error()
}
return b.String()
}
// toJSON takes an interface, marshals it to json, and returns a string. It will
// always return a string, even on marshal error (empty string).
//
// This is designed to be called from a template.
func toJSON(v interface{}) string {
data, err := json.Marshal(v)
if err != nil {
// Swallow errors inside of a template.
return ""
}
return string(data)
}
// fromJSON converts a JSON document into a map[string]interface{}.
//
// This is not a general-purpose JSON parser, and will not parse all valid
// JSON documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string into
// m["Error"] in the returned map.
func fromJSON(str string) map[string]interface{} {
m := make(map[string]interface{})
if err := json.Unmarshal([]byte(str), &m); err != nil {
m["Error"] = err.Error()
}
return m
}
// fromJSONArray converts a JSON array into a []interface{}.
//
// This is not a general-purpose JSON parser, and will not parse all valid
// JSON documents. Additionally, because its intended use is within templates
// it tolerates errors. It will insert the returned error message string as
// the first and only item in the returned array.
func fromJSONArray(str string) []interface{} {
a := []interface{}{}
if err := json.Unmarshal([]byte(str), &a); err != nil {
a = []interface{}{err.Error()}
}
return a
}

View File

@@ -0,0 +1,124 @@
/*
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 engine
import (
"context"
"log"
"strings"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
)
type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error)
// NewLookupFunction returns a function for looking up objects in the cluster.
//
// If the resource does not exist, no error is raised.
//
// This function is considered deprecated, and will be renamed in Helm 4. It will no
// longer be a public function.
func NewLookupFunction(config *rest.Config) lookupFunc {
return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) {
var client dynamic.ResourceInterface
c, namespaced, err := getDynamicClientOnKind(apiversion, resource, config)
if err != nil {
return map[string]interface{}{}, err
}
if namespaced && namespace != "" {
client = c.Namespace(namespace)
} else {
client = c
}
if name != "" {
// this will return a single object
obj, err := client.Get(context.Background(), name, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
// Just return an empty interface when the object was not found.
// That way, users can use `if not (lookup ...)` in their templates.
return map[string]interface{}{}, nil
}
return map[string]interface{}{}, err
}
return obj.UnstructuredContent(), nil
}
//this will return a list
obj, err := client.List(context.Background(), metav1.ListOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
// Just return an empty interface when the object was not found.
// That way, users can use `if not (lookup ...)` in their templates.
return map[string]interface{}{}, nil
}
return map[string]interface{}{}, err
}
return obj.UnstructuredContent(), nil
}
}
// getDynamicClientOnUnstructured returns a dynamic client on an Unstructured type. This client can be further namespaced.
func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) {
gvk := schema.FromAPIVersionAndKind(apiversion, kind)
apiRes, err := getAPIResourceForGVK(gvk, config)
if err != nil {
log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err)
return nil, false, errors.Wrapf(err, "unable to get apiresource from unstructured: %s", gvk.String())
}
gvr := schema.GroupVersionResource{
Group: apiRes.Group,
Version: apiRes.Version,
Resource: apiRes.Name,
}
intf, err := dynamic.NewForConfig(config)
if err != nil {
log.Printf("[ERROR] unable to get dynamic client %s", err)
return nil, false, err
}
res := intf.Resource(gvr)
return res, apiRes.Namespaced, nil
}
func getAPIResourceForGVK(gvk schema.GroupVersionKind, config *rest.Config) (metav1.APIResource, error) {
res := metav1.APIResource{}
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
log.Printf("[ERROR] unable to create discovery client %s", err)
return res, err
}
resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String())
if err != nil {
log.Printf("[ERROR] unable to retrieve resource list for: %s , error: %s", gvk.GroupVersion().String(), err)
return res, err
}
for _, resource := range resList.APIResources {
//if a resource contains a "/" it's referencing a subresource. we don't support suberesource for now.
if resource.Kind == gvk.Kind && !strings.Contains(resource.Name, "/") {
res = resource
res.Group = gvk.Group
res.Version = gvk.Version
break
}
}
return res, nil
}

20
vendor/helm.sh/helm/v3/pkg/gates/doc.go vendored Normal file
View File

@@ -0,0 +1,20 @@
/*
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 gates provides a general tool for working with experimental feature gates.
This provides convenience methods where the user can determine if certain experimental features are enabled.
*/
package gates

View File

@@ -0,0 +1,38 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package gates
import (
"fmt"
"os"
)
// Gate is the name of the feature gate.
type Gate string
// String returns the string representation of this feature gate.
func (g Gate) String() string {
return string(g)
}
// IsEnabled determines whether a certain feature gate is enabled.
func (g Gate) IsEnabled() bool {
return os.Getenv(string(g)) != ""
}
func (g Gate) Error() error {
return fmt.Errorf("this feature has been marked as experimental and is not enabled by default. Please set %s=1 in your environment to use this feature", g.String())
}

View File

@@ -38,6 +38,7 @@ type options struct {
insecureSkipVerifyTLS bool
username string
password string
passCredentialsAll bool
userAgent string
version string
registryClient *registry.Client
@@ -64,6 +65,12 @@ func WithBasicAuth(username, password string) Option {
}
}
func WithPassCredentialsAll(pass bool) Option {
return func(opts *options) {
opts.passCredentialsAll = pass
}
}
// WithUserAgent sets the request's User-Agent header to use the provided agent name.
func WithUserAgent(userAgent string) Option {
return func(opts *options) {

View File

@@ -20,6 +20,7 @@ import (
"crypto/tls"
"io"
"net/http"
"net/url"
"github.com/pkg/errors"
@@ -56,8 +57,24 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) {
req.Header.Set("User-Agent", g.opts.userAgent)
}
if g.opts.username != "" && g.opts.password != "" {
req.SetBasicAuth(g.opts.username, g.opts.password)
// Before setting the basic auth credentials, make sure the URL associated
// with the basic auth is the one being fetched.
u1, err := url.Parse(g.opts.url)
if err != nil {
return buf, errors.Wrap(err, "Unable to parse getter URL")
}
u2, err := url.Parse(href)
if err != nil {
return buf, errors.Wrap(err, "Unable to parse URL getting from")
}
// Host on URL (returned from url.Parse) contains the port if present.
// This check ensures credentials are not passed between different
// services on different ports.
if g.opts.passCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) {
if g.opts.username != "" && g.opts.password != "" {
req.SetBasicAuth(g.opts.username, g.opts.password)
}
}
client, err := g.httpClient()

View File

@@ -58,10 +58,19 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) {
}
// NewOCIGetter constructs a valid http/https client as a Getter
func NewOCIGetter(options ...Option) (Getter, error) {
var client OCIGetter
func NewOCIGetter(ops ...Option) (Getter, error) {
registryClient, err := registry.NewClient()
if err != nil {
return nil, err
}
for _, opt := range options {
client := OCIGetter{
opts: options{
registryClient: registryClient,
},
}
for _, opt := range ops {
opt(&client.opts)
}

View File

@@ -21,6 +21,8 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
@@ -55,6 +57,10 @@ var ErrNoObjectsVisited = errors.New("no objects visited")
var metadataAccessor = meta.NewAccessor()
// ManagedFieldsManager is the name of the manager of Kubernetes managedFields
// first introduced in Kubernetes 1.18
var ManagedFieldsManager string
// Client represents a client capable of communicating with the Kubernetes API.
type Client struct {
Factory Factory
@@ -100,7 +106,7 @@ func (c *Client) getKubeClient() (*kubernetes.Clientset, error) {
return c.kubeClient, err
}
// IsReachable tests connectivity to the cluster
// IsReachable tests connectivity to the cluster.
func (c *Client) IsReachable() error {
client, err := c.getKubeClient()
if err == genericclioptions.ErrEmptyConfig {
@@ -126,18 +132,19 @@ func (c *Client) Create(resources ResourceList) (*Result, error) {
return &Result{Created: resources}, nil
}
// Wait up to the given timeout for the specified resources to be ready
// Wait waits up to the given timeout for the specified resources to be ready.
func (c *Client) Wait(resources ResourceList, timeout time.Duration) error {
cs, err := c.getKubeClient()
if err != nil {
return err
}
checker := NewReadyChecker(cs, c.Log, PausedAsReady(true))
w := waiter{
c: cs,
c: checker,
log: c.Log,
timeout: timeout,
}
return w.waitForResources(resources, false)
return w.waitForResources(resources)
}
// WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs.
@@ -146,12 +153,13 @@ func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) err
if err != nil {
return err
}
checker := NewReadyChecker(cs, c.Log, PausedAsReady(true), CheckJobs(true))
w := waiter{
c: cs,
c: checker,
log: c.Log,
timeout: timeout,
}
return w.waitForResources(resources, true)
return w.waitForResources(resources)
}
func (c *Client) namespace() string {
@@ -204,7 +212,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
return err
}
helper := resource.NewHelper(info.Client, info.Mapping)
helper := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager())
if _, err := helper.Get(info.Namespace, info.Name); err != nil {
if !apierrors.IsNotFound(err) {
return errors.Wrap(err, "could not get information about the resource")
@@ -322,7 +330,7 @@ func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error {
// WatchUntilReady watches the resources given and waits until it is ready.
//
// This function is mainly for hook implementations. It watches for a resource to
// This method is mainly for hook implementations. It watches for a resource to
// hit a particular milestone. The milestone depends on the Kind.
//
// For most kinds, it checks to see if the resource is marked as Added or Modified
@@ -357,6 +365,26 @@ func perform(infos ResourceList, fn func(*resource.Info) error) error {
return nil
}
// getManagedFieldsManager returns the manager string. If one was set it will be returned.
// Otherwise, one is calculated based on the name of the binary.
func getManagedFieldsManager() string {
// When a manager is explicitly set use it
if ManagedFieldsManager != "" {
return ManagedFieldsManager
}
// When no manager is set and no calling application can be found it is unknown
if len(os.Args[0]) == 0 {
return "unknown"
}
// When there is an application that can be determined and no set manager
// use the base name. This is one of the ways Kubernetes libs handle figuring
// names out.
return filepath.Base(os.Args[0])
}
func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- error) {
var kind string
var wg sync.WaitGroup
@@ -375,7 +403,7 @@ func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<-
}
func createResource(info *resource.Info) error {
obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
obj, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).Create(info.Namespace, true, info.Object)
if err != nil {
return err
}
@@ -385,7 +413,7 @@ func createResource(info *resource.Info) error {
func deleteResource(info *resource.Info) error {
policy := metav1.DeletePropagationBackground
opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
_, err := resource.NewHelper(info.Client, info.Mapping).DeleteWithOptions(info.Namespace, info.Name, opts)
_, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts)
return err
}
@@ -400,7 +428,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P
}
// Fetch the current object for the three way merge
helper := resource.NewHelper(target.Client, target.Mapping)
helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
currentObj, err := helper.Get(target.Namespace, target.Name)
if err != nil && !apierrors.IsNotFound(err) {
return nil, types.StrategicMergePatchType, errors.Wrapf(err, "unable to get data for current object %s/%s", target.Namespace, target.Name)
@@ -442,7 +470,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P
func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error {
var (
obj runtime.Object
helper = resource.NewHelper(target.Client, target.Mapping)
helper = resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
kind = target.Mapping.GroupVersionKind.Kind
)

View File

@@ -0,0 +1,107 @@
/*
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 fake implements various fake KubeClients for use in testing
package fake
import (
"io"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v3/pkg/kube"
)
// FailingKubeClient implements KubeClient for testing purposes. It also has
// additional errors you can set to fail different functions, otherwise it
// delegates all its calls to `PrintingKubeClient`
type FailingKubeClient struct {
PrintingKubeClient
CreateError error
WaitError error
DeleteError error
WatchUntilReadyError error
UpdateError error
BuildError error
BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error
}
// Create returns the configured error if set or prints
func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, error) {
if f.CreateError != nil {
return nil, f.CreateError
}
return f.PrintingKubeClient.Create(resources)
}
// Wait returns the configured error if set or prints
func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error {
if f.WaitError != nil {
return f.WaitError
}
return f.PrintingKubeClient.Wait(resources, d)
}
// WaitWithJobs returns the configured error if set or prints
func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Duration) error {
if f.WaitError != nil {
return f.WaitError
}
return f.PrintingKubeClient.Wait(resources, d)
}
// Delete returns the configured error if set or prints
func (f *FailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) {
if f.DeleteError != nil {
return nil, []error{f.DeleteError}
}
return f.PrintingKubeClient.Delete(resources)
}
// WatchUntilReady returns the configured error if set or prints
func (f *FailingKubeClient) WatchUntilReady(resources kube.ResourceList, d time.Duration) error {
if f.WatchUntilReadyError != nil {
return f.WatchUntilReadyError
}
return f.PrintingKubeClient.WatchUntilReady(resources, d)
}
// Update returns the configured error if set or prints
func (f *FailingKubeClient) Update(r, modified kube.ResourceList, ignoreMe bool) (*kube.Result, error) {
if f.UpdateError != nil {
return &kube.Result{}, f.UpdateError
}
return f.PrintingKubeClient.Update(r, modified, ignoreMe)
}
// Build returns the configured error if set or prints
func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error) {
if f.BuildError != nil {
return []*resource.Info{}, f.BuildError
}
return f.PrintingKubeClient.Build(r, false)
}
// WaitAndGetCompletedPodPhase returns the configured error if set or prints
func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duration) (v1.PodPhase, error) {
if f.WaitAndGetCompletedPodPhaseError != nil {
return v1.PodSucceeded, f.WaitAndGetCompletedPodPhaseError
}
return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d)
}

View File

@@ -0,0 +1,105 @@
/*
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 fake
import (
"io"
"strings"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/v3/pkg/kube"
)
// PrintingKubeClient implements KubeClient, but simply prints the reader to
// the given output.
type PrintingKubeClient struct {
Out io.Writer
}
// IsReachable checks if the cluster is reachable
func (p *PrintingKubeClient) IsReachable() error {
return nil
}
// Create prints the values of what would be created with a real KubeClient.
func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, err
}
return &kube.Result{Created: resources}, nil
}
func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources))
return err
}
func (p *PrintingKubeClient) WaitWithJobs(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources))
return err
}
// Delete implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}
}
return &kube.Result{Deleted: resources}, nil
}
// WatchUntilReady implements KubeClient WatchUntilReady.
func (p *PrintingKubeClient) WatchUntilReady(resources kube.ResourceList, _ time.Duration) error {
_, err := io.Copy(p.Out, bufferize(resources))
return err
}
// Update implements KubeClient Update.
func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ bool) (*kube.Result, error) {
_, err := io.Copy(p.Out, bufferize(modified))
if err != nil {
return nil, err
}
// TODO: This doesn't completely mock out have some that get created,
// updated, and deleted. I don't think these are used in any unit tests, but
// we may want to refactor a way to handle future tests
return &kube.Result{Updated: modified}, nil
}
// Build implements KubeClient Build.
func (p *PrintingKubeClient) Build(_ io.Reader, _ bool) (kube.ResourceList, error) {
return []*resource.Info{}, nil
}
// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase.
func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (v1.PodPhase, error) {
return v1.PodSucceeded, nil
}
func bufferize(resources kube.ResourceList) io.Reader {
var builder strings.Builder
for _, info := range resources {
builder.WriteString(info.String() + "\n")
}
return strings.NewReader(builder.String())
}

View File

@@ -30,14 +30,19 @@ type Interface interface {
// Create creates one or more resources.
Create(resources ResourceList) (*Result, error)
// Wait waits up to the given timeout for the specified resources to be ready.
Wait(resources ResourceList, timeout time.Duration) error
// WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs.
WaitWithJobs(resources ResourceList, timeout time.Duration) error
// Delete destroys one or more resources.
Delete(resources ResourceList) (*Result, []error)
// Watch the resource in reader until it is "ready". This method
// WatchUntilReady watches the resources given and waits until it is ready.
//
// This method is mainly for hook implementations. It watches for a resource to
// hit a particular milestone. The milestone depends on the Kind.
//
// For Jobs, "ready" means the Job ran to completion (exited without error).
// For Pods, "ready" means the Pod phase is marked "succeeded".
@@ -49,9 +54,9 @@ type Interface interface {
// if it doesn't exist.
Update(original, target ResourceList, force bool) (*Result, error)
// Build creates a resource list from a Reader
// Build creates a resource list from a Reader.
//
// reader must contain a YAML stream (one or more YAML documents separated
// Reader must contain a YAML stream (one or more YAML documents separated
// by "\n---\n")
//
// Validates against OpenAPI schema if validate is true.
@@ -61,7 +66,7 @@ type Interface interface {
// and returns said phase (PodSucceeded or PodFailed qualify).
WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error)
// isReachable checks whether the client is able to connect to the cluster
// IsReachable checks whether the client is able to connect to the cluster.
IsReachable() error
}

397
vendor/helm.sh/helm/v3/pkg/kube/ready.go vendored Normal file
View File

@@ -0,0 +1,397 @@
/*
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 kube // import "helm.sh/helm/v3/pkg/kube"
import (
"context"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
deploymentutil "helm.sh/helm/v3/internal/third_party/k8s.io/kubernetes/deployment/util"
)
// ReadyCheckerOption is a function that configures a ReadyChecker.
type ReadyCheckerOption func(*ReadyChecker)
// PausedAsReady returns a ReadyCheckerOption that configures a ReadyChecker
// to consider paused resources to be ready. For example a Deployment
// with spec.paused equal to true would be considered ready.
func PausedAsReady(pausedAsReady bool) ReadyCheckerOption {
return func(c *ReadyChecker) {
c.pausedAsReady = pausedAsReady
}
}
// CheckJobs returns a ReadyCheckerOption that configures a ReadyChecker
// to consider readiness of Job resources.
func CheckJobs(checkJobs bool) ReadyCheckerOption {
return func(c *ReadyChecker) {
c.checkJobs = checkJobs
}
}
// NewReadyChecker creates a new checker. Passed ReadyCheckerOptions can
// be used to override defaults.
func NewReadyChecker(cl kubernetes.Interface, log func(string, ...interface{}), opts ...ReadyCheckerOption) ReadyChecker {
c := ReadyChecker{
client: cl,
log: log,
}
if c.log == nil {
c.log = nopLogger
}
for _, opt := range opts {
opt(&c)
}
return c
}
// ReadyChecker is a type that can check core Kubernetes types for readiness.
type ReadyChecker struct {
client kubernetes.Interface
log func(string, ...interface{})
checkJobs bool
pausedAsReady bool
}
// IsReady checks if v is ready. It supports checking readiness for pods,
// deployments, persistent volume claims, services, daemon sets, custom
// resource definitions, stateful sets, replication controllers, and replica
// sets. All other resource kinds are always considered ready.
//
// IsReady will fetch the latest state of the object from the server prior to
// performing readiness checks, and it will return any error encountered.
func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, error) {
var (
// This defaults to true, otherwise we get to a point where
// things will always return false unless one of the objects
// that manages pods has been hit
ok = true
err error
)
switch value := AsVersioned(v).(type) {
case *corev1.Pod:
pod, err := c.client.CoreV1().Pods(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil || !c.isPodReady(pod) {
return false, err
}
case *batchv1.Job:
if c.checkJobs {
job, err := c.client.BatchV1().Jobs(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil || !c.jobReady(job) {
return false, err
}
}
case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment:
currentDeployment, err := c.client.AppsV1().Deployments(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
// If paused deployment will never be ready
if currentDeployment.Spec.Paused {
return c.pausedAsReady, nil
}
// Find RS associated with deployment
newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, c.client.AppsV1())
if err != nil || newReplicaSet == nil {
return false, err
}
if !c.deploymentReady(newReplicaSet, currentDeployment) {
return false, nil
}
case *corev1.PersistentVolumeClaim:
claim, err := c.client.CoreV1().PersistentVolumeClaims(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !c.volumeReady(claim) {
return false, nil
}
case *corev1.Service:
svc, err := c.client.CoreV1().Services(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !c.serviceReady(svc) {
return false, nil
}
case *extensionsv1beta1.DaemonSet, *appsv1.DaemonSet, *appsv1beta2.DaemonSet:
ds, err := c.client.AppsV1().DaemonSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !c.daemonSetReady(ds) {
return false, nil
}
case *apiextv1beta1.CustomResourceDefinition:
if err := v.Get(); err != nil {
return false, err
}
crd := &apiextv1beta1.CustomResourceDefinition{}
if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil {
return false, err
}
if !c.crdBetaReady(*crd) {
return false, nil
}
case *apiextv1.CustomResourceDefinition:
if err := v.Get(); err != nil {
return false, err
}
crd := &apiextv1.CustomResourceDefinition{}
if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil {
return false, err
}
if !c.crdReady(*crd) {
return false, nil
}
case *appsv1.StatefulSet, *appsv1beta1.StatefulSet, *appsv1beta2.StatefulSet:
sts, err := c.client.AppsV1().StatefulSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !c.statefulSetReady(sts) {
return false, nil
}
case *corev1.ReplicationController, *extensionsv1beta1.ReplicaSet, *appsv1beta2.ReplicaSet, *appsv1.ReplicaSet:
ok, err = c.podsReadyForObject(ctx, v.Namespace, value)
}
if !ok || err != nil {
return false, err
}
return true, nil
}
func (c *ReadyChecker) podsReadyForObject(ctx context.Context, namespace string, obj runtime.Object) (bool, error) {
pods, err := c.podsforObject(ctx, namespace, obj)
if err != nil {
return false, err
}
for _, pod := range pods {
if !c.isPodReady(&pod) {
return false, nil
}
}
return true, nil
}
func (c *ReadyChecker) podsforObject(ctx context.Context, namespace string, obj runtime.Object) ([]corev1.Pod, error) {
selector, err := SelectorsForObject(obj)
if err != nil {
return nil, err
}
list, err := getPods(ctx, c.client, namespace, selector.String())
return list, err
}
// isPodReady returns true if a pod is ready; false otherwise.
func (c *ReadyChecker) isPodReady(pod *corev1.Pod) bool {
for _, c := range pod.Status.Conditions {
if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue {
return true
}
}
c.log("Pod is not ready: %s/%s", pod.GetNamespace(), pod.GetName())
return false
}
func (c *ReadyChecker) jobReady(job *batchv1.Job) bool {
if job.Status.Failed > *job.Spec.BackoffLimit {
c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName())
return false
}
if job.Status.Succeeded < *job.Spec.Completions {
c.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName())
return false
}
return true
}
func (c *ReadyChecker) serviceReady(s *corev1.Service) bool {
// ExternalName Services are external to cluster so helm shouldn't be checking to see if they're 'ready' (i.e. have an IP Set)
if s.Spec.Type == corev1.ServiceTypeExternalName {
return true
}
// Ensure that the service cluster IP is not empty
if s.Spec.ClusterIP == "" {
c.log("Service does not have cluster IP address: %s/%s", s.GetNamespace(), s.GetName())
return false
}
// This checks if the service has a LoadBalancer and that balancer has an Ingress defined
if s.Spec.Type == corev1.ServiceTypeLoadBalancer {
// do not wait when at least 1 external IP is set
if len(s.Spec.ExternalIPs) > 0 {
c.log("Service %s/%s has external IP addresses (%v), marking as ready", s.GetNamespace(), s.GetName(), s.Spec.ExternalIPs)
return true
}
if s.Status.LoadBalancer.Ingress == nil {
c.log("Service does not have load balancer ingress IP address: %s/%s", s.GetNamespace(), s.GetName())
return false
}
}
return true
}
func (c *ReadyChecker) volumeReady(v *corev1.PersistentVolumeClaim) bool {
if v.Status.Phase != corev1.ClaimBound {
c.log("PersistentVolumeClaim is not bound: %s/%s", v.GetNamespace(), v.GetName())
return false
}
return true
}
func (c *ReadyChecker) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deployment) bool {
expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep)
if !(rs.Status.ReadyReplicas >= expectedReady) {
c.log("Deployment is not ready: %s/%s. %d out of %d expected pods are ready", dep.Namespace, dep.Name, rs.Status.ReadyReplicas, expectedReady)
return false
}
return true
}
func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool {
// If the update strategy is not a rolling update, there will be nothing to wait for
if ds.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType {
return true
}
// Make sure all the updated pods have been scheduled
if ds.Status.UpdatedNumberScheduled != ds.Status.DesiredNumberScheduled {
c.log("DaemonSet is not ready: %s/%s. %d out of %d expected pods have been scheduled", ds.Namespace, ds.Name, ds.Status.UpdatedNumberScheduled, ds.Status.DesiredNumberScheduled)
return false
}
maxUnavailable, err := intstr.GetValueFromIntOrPercent(ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable, int(ds.Status.DesiredNumberScheduled), true)
if err != nil {
// If for some reason the value is invalid, set max unavailable to the
// number of desired replicas. This is the same behavior as the
// `MaxUnavailable` function in deploymentutil
maxUnavailable = int(ds.Status.DesiredNumberScheduled)
}
expectedReady := int(ds.Status.DesiredNumberScheduled) - maxUnavailable
if !(int(ds.Status.NumberReady) >= expectedReady) {
c.log("DaemonSet is not ready: %s/%s. %d out of %d expected pods are ready", ds.Namespace, ds.Name, ds.Status.NumberReady, expectedReady)
return false
}
return true
}
// Because the v1 extensions API is not available on all supported k8s versions
// yet and because Go doesn't support generics, we need to have a duplicate
// function to support the v1beta1 types
func (c *ReadyChecker) crdBetaReady(crd apiextv1beta1.CustomResourceDefinition) bool {
for _, cond := range crd.Status.Conditions {
switch cond.Type {
case apiextv1beta1.Established:
if cond.Status == apiextv1beta1.ConditionTrue {
return true
}
case apiextv1beta1.NamesAccepted:
if cond.Status == apiextv1beta1.ConditionFalse {
// This indicates a naming conflict, but it's probably not the
// job of this function to fail because of that. Instead,
// we treat it as a success, since the process should be able to
// continue.
return true
}
}
}
return false
}
func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool {
for _, cond := range crd.Status.Conditions {
switch cond.Type {
case apiextv1.Established:
if cond.Status == apiextv1.ConditionTrue {
return true
}
case apiextv1.NamesAccepted:
if cond.Status == apiextv1.ConditionFalse {
// This indicates a naming conflict, but it's probably not the
// job of this function to fail because of that. Instead,
// we treat it as a success, since the process should be able to
// continue.
return true
}
}
}
return false
}
func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
// If the update strategy is not a rolling update, there will be nothing to wait for
if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType {
return true
}
// Dereference all the pointers because StatefulSets like them
var partition int
// 1 is the default for replicas if not set
var replicas = 1
// For some reason, even if the update strategy is a rolling update, the
// actual rollingUpdate field can be nil. If it is, we can safely assume
// there is no partition value
if sts.Spec.UpdateStrategy.RollingUpdate != nil && sts.Spec.UpdateStrategy.RollingUpdate.Partition != nil {
partition = int(*sts.Spec.UpdateStrategy.RollingUpdate.Partition)
}
if sts.Spec.Replicas != nil {
replicas = int(*sts.Spec.Replicas)
}
// Because an update strategy can use partitioning, we need to calculate the
// number of updated replicas we should have. For example, if the replicas
// is set to 3 and the partition is 2, we'd expect only one pod to be
// updated
expectedReplicas := replicas - partition
// Make sure all the updated pods have been scheduled
if int(sts.Status.UpdatedReplicas) != expectedReplicas {
c.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods have been scheduled", sts.Namespace, sts.Name, sts.Status.UpdatedReplicas, expectedReplicas)
return false
}
if int(sts.Status.ReadyReplicas) != replicas {
c.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods are ready", sts.Namespace, sts.Name, sts.Status.ReadyReplicas, replicas)
return false
}
return true
}
func getPods(ctx context.Context, client kubernetes.Interface, namespace, selector string) ([]corev1.Pod, error) {
list, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: selector,
})
return list.Items, err
}

View File

@@ -28,339 +28,36 @@ import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
deploymentutil "helm.sh/helm/v3/internal/third_party/k8s.io/kubernetes/deployment/util"
"k8s.io/apimachinery/pkg/util/wait"
)
type waiter struct {
c kubernetes.Interface
c ReadyChecker
timeout time.Duration
log func(string, ...interface{})
}
// waitForResources polls to get the current status of all pods, PVCs, Services and
// Jobs(optional) until all are ready or a timeout is reached
func (w *waiter) waitForResources(created ResourceList, waitForJobsEnabled bool) error {
func (w *waiter) waitForResources(created ResourceList) error {
w.log("beginning wait for %d resources with timeout of %v", len(created), w.timeout)
return wait.Poll(2*time.Second, w.timeout, func() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), w.timeout)
defer cancel()
return wait.PollImmediateUntil(2*time.Second, func() (bool, error) {
for _, v := range created {
var (
// This defaults to true, otherwise we get to a point where
// things will always return false unless one of the objects
// that manages pods has been hit
ok = true
err error
)
switch value := AsVersioned(v).(type) {
case *corev1.Pod:
pod, err := w.c.CoreV1().Pods(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{})
if err != nil || !w.isPodReady(pod) {
return false, err
}
case *batchv1.Job:
if waitForJobsEnabled {
job, err := w.c.BatchV1().Jobs(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{})
if err != nil || !w.jobReady(job) {
return false, err
}
}
case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment:
currentDeployment, err := w.c.AppsV1().Deployments(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
// If paused deployment will never be ready
if currentDeployment.Spec.Paused {
continue
}
// Find RS associated with deployment
newReplicaSet, err := deploymentutil.GetNewReplicaSet(currentDeployment, w.c.AppsV1())
if err != nil || newReplicaSet == nil {
return false, err
}
if !w.deploymentReady(newReplicaSet, currentDeployment) {
return false, nil
}
case *corev1.PersistentVolumeClaim:
claim, err := w.c.CoreV1().PersistentVolumeClaims(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !w.volumeReady(claim) {
return false, nil
}
case *corev1.Service:
svc, err := w.c.CoreV1().Services(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !w.serviceReady(svc) {
return false, nil
}
case *extensionsv1beta1.DaemonSet, *appsv1.DaemonSet, *appsv1beta2.DaemonSet:
ds, err := w.c.AppsV1().DaemonSets(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !w.daemonSetReady(ds) {
return false, nil
}
case *apiextv1beta1.CustomResourceDefinition:
if err := v.Get(); err != nil {
return false, err
}
crd := &apiextv1beta1.CustomResourceDefinition{}
if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil {
return false, err
}
if !w.crdBetaReady(*crd) {
return false, nil
}
case *apiextv1.CustomResourceDefinition:
if err := v.Get(); err != nil {
return false, err
}
crd := &apiextv1.CustomResourceDefinition{}
if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil {
return false, err
}
if !w.crdReady(*crd) {
return false, nil
}
case *appsv1.StatefulSet, *appsv1beta1.StatefulSet, *appsv1beta2.StatefulSet:
sts, err := w.c.AppsV1().StatefulSets(v.Namespace).Get(context.Background(), v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !w.statefulSetReady(sts) {
return false, nil
}
case *corev1.ReplicationController, *extensionsv1beta1.ReplicaSet, *appsv1beta2.ReplicaSet, *appsv1.ReplicaSet:
ok, err = w.podsReadyForObject(v.Namespace, value)
}
if !ok || err != nil {
ready, err := w.c.IsReady(ctx, v)
if !ready || err != nil {
return false, err
}
}
return true, nil
})
}
func (w *waiter) podsReadyForObject(namespace string, obj runtime.Object) (bool, error) {
pods, err := w.podsforObject(namespace, obj)
if err != nil {
return false, err
}
for _, pod := range pods {
if !w.isPodReady(&pod) {
return false, nil
}
}
return true, nil
}
func (w *waiter) podsforObject(namespace string, obj runtime.Object) ([]corev1.Pod, error) {
selector, err := SelectorsForObject(obj)
if err != nil {
return nil, err
}
list, err := getPods(w.c, namespace, selector.String())
return list, err
}
// isPodReady returns true if a pod is ready; false otherwise.
func (w *waiter) isPodReady(pod *corev1.Pod) bool {
for _, c := range pod.Status.Conditions {
if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue {
return true
}
}
w.log("Pod is not ready: %s/%s", pod.GetNamespace(), pod.GetName())
return false
}
func (w *waiter) jobReady(job *batchv1.Job) bool {
if job.Status.Failed >= *job.Spec.BackoffLimit {
w.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName())
return false
}
if job.Status.Succeeded < *job.Spec.Completions {
w.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName())
return false
}
return true
}
func (w *waiter) serviceReady(s *corev1.Service) bool {
// ExternalName Services are external to cluster so helm shouldn't be checking to see if they're 'ready' (i.e. have an IP Set)
if s.Spec.Type == corev1.ServiceTypeExternalName {
return true
}
// Ensure that the service cluster IP is not empty
if s.Spec.ClusterIP == "" {
w.log("Service does not have cluster IP address: %s/%s", s.GetNamespace(), s.GetName())
return false
}
// This checks if the service has a LoadBalancer and that balancer has an Ingress defined
if s.Spec.Type == corev1.ServiceTypeLoadBalancer {
// do not wait when at least 1 external IP is set
if len(s.Spec.ExternalIPs) > 0 {
w.log("Service %s/%s has external IP addresses (%v), marking as ready", s.GetNamespace(), s.GetName(), s.Spec.ExternalIPs)
return true
}
if s.Status.LoadBalancer.Ingress == nil {
w.log("Service does not have load balancer ingress IP address: %s/%s", s.GetNamespace(), s.GetName())
return false
}
}
return true
}
func (w *waiter) volumeReady(v *corev1.PersistentVolumeClaim) bool {
if v.Status.Phase != corev1.ClaimBound {
w.log("PersistentVolumeClaim is not bound: %s/%s", v.GetNamespace(), v.GetName())
return false
}
return true
}
func (w *waiter) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deployment) bool {
expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep)
if !(rs.Status.ReadyReplicas >= expectedReady) {
w.log("Deployment is not ready: %s/%s. %d out of %d expected pods are ready", dep.Namespace, dep.Name, rs.Status.ReadyReplicas, expectedReady)
return false
}
return true
}
func (w *waiter) daemonSetReady(ds *appsv1.DaemonSet) bool {
// If the update strategy is not a rolling update, there will be nothing to wait for
if ds.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType {
return true
}
// Make sure all the updated pods have been scheduled
if ds.Status.UpdatedNumberScheduled != ds.Status.DesiredNumberScheduled {
w.log("DaemonSet is not ready: %s/%s. %d out of %d expected pods have been scheduled", ds.Namespace, ds.Name, ds.Status.UpdatedNumberScheduled, ds.Status.DesiredNumberScheduled)
return false
}
maxUnavailable, err := intstr.GetValueFromIntOrPercent(ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable, int(ds.Status.DesiredNumberScheduled), true)
if err != nil {
// If for some reason the value is invalid, set max unavailable to the
// number of desired replicas. This is the same behavior as the
// `MaxUnavailable` function in deploymentutil
maxUnavailable = int(ds.Status.DesiredNumberScheduled)
}
expectedReady := int(ds.Status.DesiredNumberScheduled) - maxUnavailable
if !(int(ds.Status.NumberReady) >= expectedReady) {
w.log("DaemonSet is not ready: %s/%s. %d out of %d expected pods are ready", ds.Namespace, ds.Name, ds.Status.NumberReady, expectedReady)
return false
}
return true
}
// Because the v1 extensions API is not available on all supported k8s versions
// yet and because Go doesn't support generics, we need to have a duplicate
// function to support the v1beta1 types
func (w *waiter) crdBetaReady(crd apiextv1beta1.CustomResourceDefinition) bool {
for _, cond := range crd.Status.Conditions {
switch cond.Type {
case apiextv1beta1.Established:
if cond.Status == apiextv1beta1.ConditionTrue {
return true
}
case apiextv1beta1.NamesAccepted:
if cond.Status == apiextv1beta1.ConditionFalse {
// This indicates a naming conflict, but it's probably not the
// job of this function to fail because of that. Instead,
// we treat it as a success, since the process should be able to
// continue.
return true
}
}
}
return false
}
func (w *waiter) crdReady(crd apiextv1.CustomResourceDefinition) bool {
for _, cond := range crd.Status.Conditions {
switch cond.Type {
case apiextv1.Established:
if cond.Status == apiextv1.ConditionTrue {
return true
}
case apiextv1.NamesAccepted:
if cond.Status == apiextv1.ConditionFalse {
// This indicates a naming conflict, but it's probably not the
// job of this function to fail because of that. Instead,
// we treat it as a success, since the process should be able to
// continue.
return true
}
}
}
return false
}
func (w *waiter) statefulSetReady(sts *appsv1.StatefulSet) bool {
// If the update strategy is not a rolling update, there will be nothing to wait for
if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType {
return true
}
// Dereference all the pointers because StatefulSets like them
var partition int
// 1 is the default for replicas if not set
var replicas = 1
// For some reason, even if the update strategy is a rolling update, the
// actual rollingUpdate field can be nil. If it is, we can safely assume
// there is no partition value
if sts.Spec.UpdateStrategy.RollingUpdate != nil && sts.Spec.UpdateStrategy.RollingUpdate.Partition != nil {
partition = int(*sts.Spec.UpdateStrategy.RollingUpdate.Partition)
}
if sts.Spec.Replicas != nil {
replicas = int(*sts.Spec.Replicas)
}
// Because an update strategy can use partitioning, we need to calculate the
// number of updated replicas we should have. For example, if the replicas
// is set to 3 and the partition is 2, we'd expect only one pod to be
// updated
expectedReplicas := replicas - partition
// Make sure all the updated pods have been scheduled
if int(sts.Status.UpdatedReplicas) != expectedReplicas {
w.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods have been scheduled", sts.Namespace, sts.Name, sts.Status.UpdatedReplicas, expectedReplicas)
return false
}
if int(sts.Status.ReadyReplicas) != replicas {
w.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods are ready", sts.Namespace, sts.Name, sts.Status.ReadyReplicas, replicas)
return false
}
return true
}
func getPods(client kubernetes.Interface, namespace, selector string) ([]corev1.Pod, error) {
list, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{
LabelSelector: selector,
})
return list.Items, err
}, ctx.Done())
}
// SelectorsForObject returns the pod label selector for a given object

37
vendor/helm.sh/helm/v3/pkg/lint/lint.go vendored Normal file
View File

@@ -0,0 +1,37 @@
/*
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 lint // import "helm.sh/helm/v3/pkg/lint"
import (
"path/filepath"
"helm.sh/helm/v3/pkg/lint/rules"
"helm.sh/helm/v3/pkg/lint/support"
)
// All runs all of the available linters on the given base directory.
func All(basedir string, values map[string]interface{}, namespace string, strict bool) support.Linter {
// Using abs path to get directory context
chartDir, _ := filepath.Abs(basedir)
linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.ValuesWithOverrides(&linter, values)
rules.Templates(&linter, values, namespace, strict)
rules.Dependencies(&linter)
return linter
}

View File

@@ -0,0 +1,210 @@
/*
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 rules // import "helm.sh/helm/v3/pkg/lint/rules"
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint/support"
)
// Chartfile runs a set of linter rules related to Chart.yaml file
func Chartfile(linter *support.Linter) {
chartFileName := "Chart.yaml"
chartPath := filepath.Join(linter.ChartDir, chartFileName)
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath))
chartFile, err := chartutil.LoadChartfile(chartPath)
validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err))
// Guard clause. Following linter rules require a parsable ChartFile
if !validChartFile {
return
}
// type check for Chart.yaml . ignoring error as any parse
// errors would already be caught in the above load function
chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath)
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile))
// Chart metadata
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile))
linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile))
linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile))
}
func validateChartVersionType(data map[string]interface{}) error {
return isStringValue(data, "version")
}
func validateChartAppVersionType(data map[string]interface{}) error {
return isStringValue(data, "appVersion")
}
func isStringValue(data map[string]interface{}, key string) error {
value, ok := data[key]
if !ok {
return nil
}
valueType := fmt.Sprintf("%T", value)
if valueType != "string" {
return errors.Errorf("%s should be of type string but it's of type %s", key, valueType)
}
return nil
}
func validateChartYamlNotDirectory(chartPath string) error {
fi, err := os.Stat(chartPath)
if err == nil && fi.IsDir() {
return errors.New("should be a file, not a directory")
}
return nil
}
func validateChartYamlFormat(chartFileError error) error {
if chartFileError != nil {
return errors.Errorf("unable to parse YAML\n\t%s", chartFileError.Error())
}
return nil
}
func validateChartName(cf *chart.Metadata) error {
if cf.Name == "" {
return errors.New("name is required")
}
return nil
}
func validateChartAPIVersion(cf *chart.Metadata) error {
if cf.APIVersion == "" {
return errors.New("apiVersion is required. The value must be either \"v1\" or \"v2\"")
}
if cf.APIVersion != chart.APIVersionV1 && cf.APIVersion != chart.APIVersionV2 {
return fmt.Errorf("apiVersion '%s' is not valid. The value must be either \"v1\" or \"v2\"", cf.APIVersion)
}
return nil
}
func validateChartVersion(cf *chart.Metadata) error {
if cf.Version == "" {
return errors.New("version is required")
}
version, err := semver.NewVersion(cf.Version)
if err != nil {
return errors.Errorf("version '%s' is not a valid SemVer", cf.Version)
}
c, err := semver.NewConstraint(">0.0.0-0")
if err != nil {
return err
}
valid, msg := c.Validate(version)
if !valid && len(msg) > 0 {
return errors.Errorf("version %v", msg[0])
}
return nil
}
func validateChartMaintainer(cf *chart.Metadata) error {
for _, maintainer := range cf.Maintainers {
if maintainer.Name == "" {
return errors.New("each maintainer requires a name")
} else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) {
return errors.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name)
} else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) {
return errors.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name)
}
}
return nil
}
func validateChartSources(cf *chart.Metadata) error {
for _, source := range cf.Sources {
if source == "" || !govalidator.IsRequestURL(source) {
return errors.Errorf("invalid source URL '%s'", source)
}
}
return nil
}
func validateChartIconPresence(cf *chart.Metadata) error {
if cf.Icon == "" {
return errors.New("icon is recommended")
}
return nil
}
func validateChartIconURL(cf *chart.Metadata) error {
if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) {
return errors.Errorf("invalid icon URL '%s'", cf.Icon)
}
return nil
}
func validateChartDependencies(cf *chart.Metadata) error {
if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV2 {
return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2)
}
return nil
}
func validateChartType(cf *chart.Metadata) error {
if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV2 {
return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2)
}
return nil
}
// loadChartFileForTypeCheck loads the Chart.yaml
// in a generic form of a map[string]interface{}, so that the type
// of the values can be checked
func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
y := make(map[string]interface{})
err = yaml.Unmarshal(b, &y)
return y, err
}

View File

@@ -0,0 +1,82 @@
/*
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 rules // import "helm.sh/helm/v3/pkg/lint/rules"
import (
"fmt"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/lint/support"
)
// Dependencies runs lints against a chart's dependencies
//
// See https://github.com/helm/helm/issues/7910
func Dependencies(linter *support.Linter) {
c, err := loader.LoadDir(linter.ChartDir)
if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) {
return
}
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c))
linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c))
}
func validateChartFormat(chartError error) error {
if chartError != nil {
return errors.Errorf("unable to load chart\n\t%s", chartError)
}
return nil
}
func validateDependencyInChartsDir(c *chart.Chart) (err error) {
dependencies := map[string]struct{}{}
missing := []string{}
for _, dep := range c.Dependencies() {
dependencies[dep.Metadata.Name] = struct{}{}
}
for _, dep := range c.Metadata.Dependencies {
if _, ok := dependencies[dep.Name]; !ok {
missing = append(missing, dep.Name)
}
}
if len(missing) > 0 {
err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ","))
}
return err
}
func validateDependencyInMetadata(c *chart.Chart) (err error) {
dependencies := map[string]struct{}{}
missing := []string{}
for _, dep := range c.Metadata.Dependencies {
dependencies[dep.Name] = struct{}{}
}
for _, dep := range c.Dependencies() {
if _, ok := dependencies[dep.Metadata.Name]; !ok {
missing = append(missing, dep.Metadata.Name)
}
}
if len(missing) > 0 {
err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ","))
}
return err
}

View File

@@ -0,0 +1,84 @@
/*
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 rules // import "helm.sh/helm/v3/pkg/lint/rules"
import (
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/deprecation"
kscheme "k8s.io/client-go/kubernetes/scheme"
)
const (
// This should be set in the Makefile based on the version of client-go being imported.
// These constants will be overwritten with LDFLAGS
k8sVersionMajor = 1
k8sVersionMinor = 20
)
// deprecatedAPIError indicates than an API is deprecated in Kubernetes
type deprecatedAPIError struct {
Deprecated string
Message string
}
func (e deprecatedAPIError) Error() string {
msg := e.Message
return msg
}
func validateNoDeprecations(resource *K8sYamlStruct) error {
// if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation
if resource.APIVersion == "" {
return nil
}
if resource.Kind == "" {
return nil
}
runtimeObject, err := resourceToRuntimeObject(resource)
if err != nil {
// do not error for non-kubernetes resources
if runtime.IsNotRegisteredError(err) {
return nil
}
return err
}
if !deprecation.IsDeprecated(runtimeObject, k8sVersionMajor, k8sVersionMinor) {
return nil
}
gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind)
return deprecatedAPIError{
Deprecated: gvk,
Message: deprecation.WarningMessage(runtimeObject),
}
}
func resourceToRuntimeObject(resource *K8sYamlStruct) (runtime.Object, error) {
scheme := runtime.NewScheme()
kscheme.AddToScheme(scheme)
gvk := schema.FromAPIVersionAndKind(resource.APIVersion, resource.Kind)
out, err := scheme.New(gvk)
if err != nil {
return nil, err
}
out.GetObjectKind().SetGroupVersionKind(gvk)
return out, nil
}

View File

@@ -0,0 +1,310 @@
/*
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 rules
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/validation"
apipath "k8s.io/apimachinery/pkg/api/validation/path"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/yaml"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/engine"
"helm.sh/helm/v3/pkg/lint/support"
)
var (
crdHookSearch = regexp.MustCompile(`"?helm\.sh/hook"?:\s+crd-install`)
releaseTimeSearch = regexp.MustCompile(`\.Release\.Time`)
)
// Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool) {
fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath)
templatesDirExist := linter.RunLinterRule(support.WarningSev, fpath, validateTemplatesDir(templatesPath))
// Templates directory is optional for now
if !templatesDirExist {
return
}
// Load chart and parse templates
chart, err := loader.Load(linter.ChartDir)
chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err)
if !chartLoaded {
return
}
options := chartutil.ReleaseOptions{
Name: "test-release",
Namespace: namespace,
}
cvals, err := chartutil.CoalesceValues(chart, values)
if err != nil {
return
}
valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, nil)
if err != nil {
linter.RunLinterRule(support.ErrorSev, fpath, err)
return
}
var e engine.Engine
e.LintMode = true
renderedContentMap, err := e.Render(chart, valuesToRender)
renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err)
if !renderOk {
return
}
/* Iterate over all the templates to check:
- It is a .yaml file
- All the values in the template file is defined
- {{}} include | quote
- Generated content is a valid Yaml file
- Metadata.Namespace is not set
*/
for _, template := range chart.Templates {
fileName, data := template.Name, template.Data
fpath = fileName
linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName))
// These are v3 specific checks to make sure and warn people if their
// chart is not compatible with v3
linter.RunLinterRule(support.WarningSev, fpath, validateNoCRDHooks(data))
linter.RunLinterRule(support.ErrorSev, fpath, validateNoReleaseTime(data))
// We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
continue
}
// NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463
// Check that all the templates have a matching value
//linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate))
// NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037
// linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate)))
renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)]
if strings.TrimSpace(renderedContent) != "" {
linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent))
decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096)
// Lint all resources if the file contains multiple documents separated by ---
for {
// Even though K8sYamlStruct only defines a few fields, an error in any other
// key will be raised as well
var yamlStruct *K8sYamlStruct
err := decoder.Decode(&yamlStruct)
if err == io.EOF {
break
}
// If YAML linting fails, we sill progress. So we don't capture the returned state
// on this linter run.
linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err))
if yamlStruct != nil {
// NOTE: set to warnings to allow users to support out-of-date kubernetes
// Refs https://github.com/helm/helm/issues/8596
linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct))
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct))
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
}
}
}
}
}
// validateTopIndentLevel checks that the content does not start with an indent level > 0.
//
// This error can occur when a template accidentally inserts space. It can cause
// unpredictable errors depending on whether the text is normalized before being passed
// into the YAML parser. So we trap it here.
//
// See https://github.com/helm/helm/issues/8467
func validateTopIndentLevel(content string) error {
// Read lines until we get to a non-empty one
scanner := bufio.NewScanner(bytes.NewBufferString(content))
for scanner.Scan() {
line := scanner.Text()
// If line is empty, skip
if strings.TrimSpace(line) == "" {
continue
}
// If it starts with one or more spaces, this is an error
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line)
}
// Any other condition passes.
return nil
}
return scanner.Err()
}
// Validation functions
func validateTemplatesDir(templatesPath string) error {
if fi, err := os.Stat(templatesPath); err != nil {
return errors.New("directory not found")
} else if !fi.IsDir() {
return errors.New("not a directory")
}
return nil
}
func validateAllowedExtension(fileName string) error {
ext := filepath.Ext(fileName)
validExtensions := []string{".yaml", ".yml", ".tpl", ".txt"}
for _, b := range validExtensions {
if b == ext {
return nil
}
}
return errors.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext)
}
func validateYamlContent(err error) error {
return errors.Wrap(err, "unable to parse YAML")
}
// validateMetadataName uses the correct validation function for the object
// Kind, or if not set, defaults to the standard definition of a subdomain in
// DNS (RFC 1123), used by most resources.
func validateMetadataName(obj *K8sYamlStruct) error {
fn := validateMetadataNameFunc(obj)
allErrs := field.ErrorList{}
for _, msg := range fn(obj.Metadata.Name, false) {
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg))
}
if len(allErrs) > 0 {
return errors.Wrapf(allErrs.ToAggregate(), "object name does not conform to Kubernetes naming requirements: %q", obj.Metadata.Name)
}
return nil
}
// validateMetadataNameFunc will return a name validation function for the
// object kind, if defined below.
//
// Rules should match those set in the various api validations:
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39
// ...
//
// Implementing here to avoid importing k/k.
//
// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object
// kinds that don't have special requirements, so is the most likely to work if
// new kinds are added.
func validateMetadataNameFunc(obj *K8sYamlStruct) validation.ValidateNameFunc {
switch strings.ToLower(obj.Kind) {
case "pod", "node", "secret", "endpoints", "resourcequota", // core
"controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps
"autoscaler", // autoscaler
"cronjob", "job", // batch
"lease", // coordination
"endpointslice", // discovery
"networkpolicy", "ingress", // networking
"podsecuritypolicy", // policy
"priorityclass", // scheduling
"podpreset", // settings
"storageclass", "volumeattachment", "csinode": // storage
return validation.NameIsDNSSubdomain
case "service":
return validation.NameIsDNS1035Label
case "namespace":
return validation.ValidateNamespaceName
case "serviceaccount":
return validation.ValidateServiceAccountName
case "certificatesigningrequest":
// No validation.
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140
return func(name string, prefix bool) []string { return nil }
case "role", "clusterrole", "rolebinding", "clusterrolebinding":
// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34
return func(name string, prefix bool) []string {
return apipath.IsValidPathSegmentName(name)
}
default:
return validation.NameIsDNSSubdomain
}
}
func validateNoCRDHooks(manifest []byte) error {
if crdHookSearch.Match(manifest) {
return errors.New("manifest is a crd-install hook. This hook is no longer supported in v3 and all CRDs should also exist the crds/ directory at the top level of the chart")
}
return nil
}
func validateNoReleaseTime(manifest []byte) error {
if releaseTimeSearch.Match(manifest) {
return errors.New(".Release.Time has been removed in v3, please replace with the `now` function in your templates")
}
return nil
}
// validateMatchSelector ensures that template specs have a selector declared.
// See https://github.com/helm/helm/issues/1990
func validateMatchSelector(yamlStruct *K8sYamlStruct, manifest string) error {
switch yamlStruct.Kind {
case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet":
// verify that matchLabels or matchExpressions is present
if !(strings.Contains(manifest, "matchLabels") || strings.Contains(manifest, "matchExpressions")) {
return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name)
}
}
return nil
}
// K8sYamlStruct stubs a Kubernetes YAML file.
//
// DEPRECATED: In Helm 4, this will be made a private type, as it is for use only within
// the rules package.
type K8sYamlStruct struct {
APIVersion string `json:"apiVersion"`
Kind string
Metadata k8sYamlMetadata
}
type k8sYamlMetadata struct {
Namespace string
Name string
}

View File

@@ -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 rules
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint/support"
)
// Values lints a chart's values.yaml file.
//
// This function is deprecated and will be removed in Helm 4.
func Values(linter *support.Linter) {
ValuesWithOverrides(linter, map[string]interface{}{})
}
// ValuesWithOverrides tests the values.yaml file.
//
// If a schema is present in the chart, values are tested against that. Otherwise,
// they are only tested for well-formedness.
//
// If additional values are supplied, they are coalesced into the values in values.yaml.
func ValuesWithOverrides(linter *support.Linter, values map[string]interface{}) {
file := "values.yaml"
vf := filepath.Join(linter.ChartDir, file)
fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf))
if !fileExists {
return
}
linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, values))
}
func validateValuesFileExistence(valuesPath string) error {
_, err := os.Stat(valuesPath)
if err != nil {
return errors.Errorf("file does not exist")
}
return nil
}
func validateValuesFile(valuesPath string, overrides map[string]interface{}) error {
values, err := chartutil.ReadValuesFile(valuesPath)
if err != nil {
return errors.Wrap(err, "unable to parse YAML")
}
// Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top
// level values against the top-level expectations. Subchart values are not linted.
// We could change that. For now, though, we retain that strategy, and thus can
// coalesce tables (like reuse-values does) instead of doing the full chart
// CoalesceValues
coalescedValues := chartutil.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides)
coalescedValues = chartutil.CoalesceTables(coalescedValues, values)
ext := filepath.Ext(valuesPath)
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
schema, err := ioutil.ReadFile(schemaPath)
if len(schema) == 0 {
return nil
}
if err != nil {
return err
}
return chartutil.ValidateAgainstSingleSchema(coalescedValues, schema)
}

View File

@@ -0,0 +1,22 @@
/*
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 support contains tools for linting charts.
Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance.
*/
package support // import "helm.sh/helm/v3/pkg/lint/support"

View File

@@ -0,0 +1,76 @@
/*
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 support
import "fmt"
// Severity indicates the severity of a Message.
const (
// UnknownSev indicates that the severity of the error is unknown, and should not stop processing.
UnknownSev = iota
// InfoSev indicates information, for example missing values.yaml file
InfoSev
// WarningSev indicates that something does not meet code standards, but will likely function.
WarningSev
// ErrorSev indicates that something will not likely function.
ErrorSev
)
// sev matches the *Sev states.
var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"}
// Linter encapsulates a linting run of a particular chart.
type Linter struct {
Messages []Message
// The highest severity of all the failing lint rules
HighestSeverity int
ChartDir string
}
// Message describes an error encountered while linting.
type Message struct {
// Severity is one of the *Sev constants
Severity int
Path string
Err error
}
func (m Message) Error() string {
return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error())
}
// NewMessage creates a new Message struct
func NewMessage(severity int, path string, err error) Message {
return Message{Severity: severity, Path: path, Err: err}
}
// RunLinterRule returns true if the validation passed
func (l *Linter) RunLinterRule(severity int, path string, err error) bool {
// severity is out of bound
if severity < 0 || severity >= len(sev) {
return false
}
if err != nil {
l.Messages = append(l.Messages, NewMessage(severity, path, err))
if severity > l.HighestSeverity {
l.HighestSeverity = severity
}
}
return err == nil
}

View File

@@ -23,6 +23,7 @@ import (
"regexp"
"runtime"
"strings"
"unicode"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
@@ -175,10 +176,25 @@ func validatePluginData(plug *Plugin, filepath string) error {
if !validPluginName.MatchString(plug.Metadata.Name) {
return fmt.Errorf("invalid plugin name at %q", filepath)
}
plug.Metadata.Usage = sanitizeString(plug.Metadata.Usage)
// We could also validate SemVer, executable, and other fields should we so choose.
return nil
}
// sanitizeString normalize spaces and removes non-printable characters.
func sanitizeString(str string) string {
return strings.Map(func(r rune) rune {
if unicode.IsSpace(r) {
return ' '
}
if unicode.IsPrint(r) {
return r
}
return -1
}, str)
}
func detectDuplicates(plugs []*Plugin) error {
names := map[string]string{}

View File

@@ -0,0 +1,108 @@
/*
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 postrender
import (
"bytes"
"io"
"os/exec"
"path/filepath"
"github.com/pkg/errors"
)
type execRender struct {
binaryPath string
}
// NewExec returns a PostRenderer implementation that calls the provided binary.
// It returns an error if the binary cannot be found. If the path does not
// contain any separators, it will search in $PATH, otherwise it will resolve
// any relative paths to a fully qualified path
func NewExec(binaryPath string) (PostRenderer, error) {
fullPath, err := getFullPath(binaryPath)
if err != nil {
return nil, err
}
return &execRender{fullPath}, nil
}
// Run the configured binary for the post render
func (p *execRender) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) {
cmd := exec.Command(p.binaryPath)
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
var postRendered = &bytes.Buffer{}
var stderr = &bytes.Buffer{}
cmd.Stdout = postRendered
cmd.Stderr = stderr
go func() {
defer stdin.Close()
io.Copy(stdin, renderedManifests)
}()
err = cmd.Run()
if err != nil {
return nil, errors.Wrapf(err, "error while running command %s. error output:\n%s", p.binaryPath, stderr.String())
}
return postRendered, nil
}
// getFullPath returns the full filepath to the binary to execute. If the path
// does not contain any separators, it will search in $PATH, otherwise it will
// resolve any relative paths to a fully qualified path
func getFullPath(binaryPath string) (string, error) {
// NOTE(thomastaylor312): I am leaving this code commented out here. During
// the implementation of post-render, it was brought up that if we are
// relying on plugins, we should actually use the plugin system so it can
// properly handle multiple OSs. This will be a feature add in the future,
// so I left this code for reference. It can be deleted or reused once the
// feature is implemented
// Manually check the plugin dir first
// if !strings.Contains(binaryPath, string(filepath.Separator)) {
// // First check the plugin dir
// pluginDir := helmpath.DataPath("plugins") // Default location
// // If location for plugins is explicitly set, check there
// if v, ok := os.LookupEnv("HELM_PLUGINS"); ok {
// pluginDir = v
// }
// // The plugins variable can actually contain multiple paths, so loop through those
// for _, p := range filepath.SplitList(pluginDir) {
// _, err := os.Stat(filepath.Join(p, binaryPath))
// if err != nil && !os.IsNotExist(err) {
// return "", err
// } else if err == nil {
// binaryPath = filepath.Join(p, binaryPath)
// break
// }
// }
// }
// Now check for the binary using the given path or check if it exists in
// the path and is executable
checkedPath, err := exec.LookPath(binaryPath)
if err != nil {
return "", errors.Wrapf(err, "unable to find binary at %s", binaryPath)
}
return filepath.Abs(checkedPath)
}

View File

@@ -0,0 +1,29 @@
/*
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 postrender contains an interface that can be implemented for custom
// post-renderers and an exec implementation that can be used for arbitrary
// binaries and scripts
package postrender
import "bytes"
type PostRenderer interface {
// Run expects a single buffer filled with Helm rendered manifests. It
// expects the modified results to be returned on a separate buffer or an
// error if there was an issue or failure while running the post render step
Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error)
}

View File

@@ -102,5 +102,5 @@ const (
HookPhaseFailed HookPhase = "Failed"
)
// Strng converts a hook phase to a printable string
// String converts a hook phase to a printable string
func (x HookPhase) String() string { return string(x) }

View File

@@ -0,0 +1,78 @@
/*
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 releaseutil // import "helm.sh/helm/v3/pkg/releaseutil"
import rspb "helm.sh/helm/v3/pkg/release"
// FilterFunc returns true if the release object satisfies
// the predicate of the underlying filter func.
type FilterFunc func(*rspb.Release) bool
// Check applies the FilterFunc to the release object.
func (fn FilterFunc) Check(rls *rspb.Release) bool {
if rls == nil {
return false
}
return fn(rls)
}
// Filter applies the filter(s) to the list of provided releases
// returning the list that satisfies the filtering predicate.
func (fn FilterFunc) Filter(rels []*rspb.Release) (rets []*rspb.Release) {
for _, rel := range rels {
if fn.Check(rel) {
rets = append(rets, rel)
}
}
return
}
// Any returns a FilterFunc that filters a list of releases
// determined by the predicate 'f0 || f1 || ... || fn'.
func Any(filters ...FilterFunc) FilterFunc {
return func(rls *rspb.Release) bool {
for _, filter := range filters {
if filter(rls) {
return true
}
}
return false
}
}
// All returns a FilterFunc that filters a list of releases
// determined by the predicate 'f0 && f1 && ... && fn'.
func All(filters ...FilterFunc) FilterFunc {
return func(rls *rspb.Release) bool {
for _, filter := range filters {
if !filter(rls) {
return false
}
}
return true
}
}
// StatusFilter filters a set of releases by status code.
func StatusFilter(status rspb.Status) FilterFunc {
return FilterFunc(func(rls *rspb.Release) bool {
if rls == nil {
return true
}
return rls.Info.Status == status
})
}

View File

@@ -0,0 +1,156 @@
/*
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 releaseutil
import (
"sort"
"helm.sh/helm/v3/pkg/release"
)
// KindSortOrder is an ordering of Kinds.
type KindSortOrder []string
// InstallOrder is the order in which manifests should be installed (by Kind).
//
// Those occurring earlier in the list get installed before those occurring later in the list.
var InstallOrder KindSortOrder = []string{
"Namespace",
"NetworkPolicy",
"ResourceQuota",
"LimitRange",
"PodSecurityPolicy",
"PodDisruptionBudget",
"ServiceAccount",
"Secret",
"SecretList",
"ConfigMap",
"StorageClass",
"PersistentVolume",
"PersistentVolumeClaim",
"CustomResourceDefinition",
"ClusterRole",
"ClusterRoleList",
"ClusterRoleBinding",
"ClusterRoleBindingList",
"Role",
"RoleList",
"RoleBinding",
"RoleBindingList",
"Service",
"DaemonSet",
"Pod",
"ReplicationController",
"ReplicaSet",
"Deployment",
"HorizontalPodAutoscaler",
"StatefulSet",
"Job",
"CronJob",
"Ingress",
"APIService",
}
// UninstallOrder is the order in which manifests should be uninstalled (by Kind).
//
// Those occurring earlier in the list get uninstalled before those occurring later in the list.
var UninstallOrder KindSortOrder = []string{
"APIService",
"Ingress",
"Service",
"CronJob",
"Job",
"StatefulSet",
"HorizontalPodAutoscaler",
"Deployment",
"ReplicaSet",
"ReplicationController",
"Pod",
"DaemonSet",
"RoleBindingList",
"RoleBinding",
"RoleList",
"Role",
"ClusterRoleBindingList",
"ClusterRoleBinding",
"ClusterRoleList",
"ClusterRole",
"CustomResourceDefinition",
"PersistentVolumeClaim",
"PersistentVolume",
"StorageClass",
"ConfigMap",
"SecretList",
"Secret",
"ServiceAccount",
"PodDisruptionBudget",
"PodSecurityPolicy",
"LimitRange",
"ResourceQuota",
"NetworkPolicy",
"Namespace",
}
// sort manifests by kind.
//
// Results are sorted by 'ordering', keeping order of items with equal kind/priority
func sortManifestsByKind(manifests []Manifest, ordering KindSortOrder) []Manifest {
sort.SliceStable(manifests, func(i, j int) bool {
return lessByKind(manifests[i], manifests[j], manifests[i].Head.Kind, manifests[j].Head.Kind, ordering)
})
return manifests
}
// sort hooks by kind, using an out-of-place sort to preserve the input parameters.
//
// Results are sorted by 'ordering', keeping order of items with equal kind/priority
func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.Hook {
h := hooks
sort.SliceStable(h, func(i, j int) bool {
return lessByKind(h[i], h[j], h[i].Kind, h[j].Kind, ordering)
})
return h
}
func lessByKind(a interface{}, b interface{}, kindA string, kindB string, o KindSortOrder) bool {
ordering := make(map[string]int, len(o))
for v, k := range o {
ordering[k] = v
}
first, aok := ordering[kindA]
second, bok := ordering[kindB]
if !aok && !bok {
// if both are unknown then sort alphabetically by kind, keep original order if same kind
if kindA != kindB {
return kindA < kindB
}
return first < second
}
// unknown kind is last
if !aok {
return false
}
if !bok {
return true
}
// sort different kinds, keep original order if same priority
return first < second
}

View File

@@ -0,0 +1,72 @@
/*
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 releaseutil
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// SimpleHead defines what the structure of the head of a manifest file
type SimpleHead struct {
Version string `json:"apiVersion"`
Kind string `json:"kind,omitempty"`
Metadata *struct {
Name string `json:"name"`
Annotations map[string]string `json:"annotations"`
} `json:"metadata,omitempty"`
}
var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
// SplitManifests takes a string of manifest and returns a map contains individual manifests
func SplitManifests(bigFile string) map[string]string {
// Basically, we're quickly splitting a stream of YAML documents into an
// array of YAML docs. The file name is just a place holder, but should be
// integer-sortable so that manifests get output in the same order as the
// input (see `BySplitManifestsOrder`).
tpl := "manifest-%d"
res := map[string]string{}
// Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly.
bigFileTmp := strings.TrimSpace(bigFile)
docs := sep.Split(bigFileTmp, -1)
var count int
for _, d := range docs {
if d == "" {
continue
}
d = strings.TrimSpace(d)
res[fmt.Sprintf(tpl, count)] = d
count = count + 1
}
return res
}
// BySplitManifestsOrder sorts by in-file manifest order, as provided in function `SplitManifests`
type BySplitManifestsOrder []string
func (a BySplitManifestsOrder) Len() int { return len(a) }
func (a BySplitManifestsOrder) Less(i, j int) bool {
// Split `manifest-%d`
anum, _ := strconv.ParseInt(a[i][len("manifest-"):], 10, 0)
bnum, _ := strconv.ParseInt(a[j][len("manifest-"):], 10, 0)
return anum < bnum
}
func (a BySplitManifestsOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

View File

@@ -0,0 +1,233 @@
/*
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 releaseutil
import (
"log"
"path"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release"
)
// Manifest represents a manifest file, which has a name and some content.
type Manifest struct {
Name string
Content string
Head *SimpleHead
}
// manifestFile represents a file that contains a manifest.
type manifestFile struct {
entries map[string]string
path string
apis chartutil.VersionSet
}
// result is an intermediate structure used during sorting.
type result struct {
hooks []*release.Hook
generic []Manifest
}
// TODO: Refactor this out. It's here because naming conventions were not followed through.
// So fix the Test hook names and then remove this.
var events = map[string]release.HookEvent{
release.HookPreInstall.String(): release.HookPreInstall,
release.HookPostInstall.String(): release.HookPostInstall,
release.HookPreDelete.String(): release.HookPreDelete,
release.HookPostDelete.String(): release.HookPostDelete,
release.HookPreUpgrade.String(): release.HookPreUpgrade,
release.HookPostUpgrade.String(): release.HookPostUpgrade,
release.HookPreRollback.String(): release.HookPreRollback,
release.HookPostRollback.String(): release.HookPostRollback,
release.HookTest.String(): release.HookTest,
// Support test-success for backward compatibility with Helm 2 tests
"test-success": release.HookTest,
}
// SortManifests takes a map of filename/YAML contents, splits the file
// by manifest entries, and sorts the entries into hook types.
//
// The resulting hooks struct will be populated with all of the generated hooks.
// Any file that does not declare one of the hook types will be placed in the
// 'generic' bucket.
//
// Files that do not parse into the expected format are simply placed into a map and
// returned.
func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) {
result := &result{}
var sortedFilePaths []string
for filePath := range files {
sortedFilePaths = append(sortedFilePaths, filePath)
}
sort.Strings(sortedFilePaths)
for _, filePath := range sortedFilePaths {
content := files[filePath]
// Skip partials. We could return these as a separate map, but there doesn't
// seem to be any need for that at this time.
if strings.HasPrefix(path.Base(filePath), "_") {
continue
}
// Skip empty files and log this.
if strings.TrimSpace(content) == "" {
continue
}
manifestFile := &manifestFile{
entries: SplitManifests(content),
path: filePath,
apis: apis,
}
if err := manifestFile.sort(result); err != nil {
return result.hooks, result.generic, err
}
}
return sortHooksByKind(result.hooks, ordering), sortManifestsByKind(result.generic, ordering), nil
}
// sort takes a manifestFile object which may contain multiple resource definition
// entries and sorts each entry by hook types, and saves the resulting hooks and
// generic manifests (or non-hooks) to the result struct.
//
// To determine hook type, it looks for a YAML structure like this:
//
// kind: SomeKind
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook: pre-install
//
// To determine the policy to delete the hook, it looks for a YAML structure like this:
//
// kind: SomeKind
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook-delete-policy: hook-succeeded
func (file *manifestFile) sort(result *result) error {
// Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys)
var sortedEntryKeys []string
for entryKey := range file.entries {
sortedEntryKeys = append(sortedEntryKeys, entryKey)
}
sort.Sort(BySplitManifestsOrder(sortedEntryKeys))
for _, entryKey := range sortedEntryKeys {
m := file.entries[entryKey]
var entry SimpleHead
if err := yaml.Unmarshal([]byte(m), &entry); err != nil {
return errors.Wrapf(err, "YAML parse error on %s", file.path)
}
if !hasAnyAnnotation(entry) {
result.generic = append(result.generic, Manifest{
Name: file.path,
Content: m,
Head: &entry,
})
continue
}
hookTypes, ok := entry.Metadata.Annotations[release.HookAnnotation]
if !ok {
result.generic = append(result.generic, Manifest{
Name: file.path,
Content: m,
Head: &entry,
})
continue
}
hw := calculateHookWeight(entry)
h := &release.Hook{
Name: entry.Metadata.Name,
Kind: entry.Kind,
Path: file.path,
Manifest: m,
Events: []release.HookEvent{},
Weight: hw,
DeletePolicies: []release.HookDeletePolicy{},
}
isUnknownHook := false
for _, hookType := range strings.Split(hookTypes, ",") {
hookType = strings.ToLower(strings.TrimSpace(hookType))
e, ok := events[hookType]
if !ok {
isUnknownHook = true
break
}
h.Events = append(h.Events, e)
}
if isUnknownHook {
log.Printf("info: skipping unknown hook: %q", hookTypes)
continue
}
result.hooks = append(result.hooks, h)
operateAnnotationValues(entry, release.HookDeleteAnnotation, func(value string) {
h.DeletePolicies = append(h.DeletePolicies, release.HookDeletePolicy(value))
})
}
return nil
}
// hasAnyAnnotation returns true if the given entry has any annotations at all.
func hasAnyAnnotation(entry SimpleHead) bool {
return entry.Metadata != nil &&
entry.Metadata.Annotations != nil &&
len(entry.Metadata.Annotations) != 0
}
// calculateHookWeight finds the weight in the hook weight annotation.
//
// If no weight is found, the assigned weight is 0
func calculateHookWeight(entry SimpleHead) int {
hws := entry.Metadata.Annotations[release.HookWeightAnnotation]
hw, err := strconv.Atoi(hws)
if err != nil {
hw = 0
}
return hw
}
// operateAnnotationValues finds the given annotation and runs the operate function with the value of that annotation
func operateAnnotationValues(entry SimpleHead, annotation string, operate func(p string)) {
if dps, ok := entry.Metadata.Annotations[annotation]; ok {
for _, dp := range strings.Split(dps, ",") {
dp = strings.ToLower(strings.TrimSpace(dp))
operate(dp)
}
}
}

View File

@@ -0,0 +1,78 @@
/*
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 releaseutil // import "helm.sh/helm/v3/pkg/releaseutil"
import (
"sort"
rspb "helm.sh/helm/v3/pkg/release"
)
type list []*rspb.Release
func (s list) Len() int { return len(s) }
func (s list) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// ByName sorts releases by name
type ByName struct{ list }
// Less compares to releases
func (s ByName) Less(i, j int) bool { return s.list[i].Name < s.list[j].Name }
// ByDate sorts releases by date
type ByDate struct{ list }
// Less compares to releases
func (s ByDate) Less(i, j int) bool {
ti := s.list[i].Info.LastDeployed.Unix()
tj := s.list[j].Info.LastDeployed.Unix()
return ti < tj
}
// ByRevision sorts releases by revision number
type ByRevision struct{ list }
// Less compares to releases
func (s ByRevision) Less(i, j int) bool {
return s.list[i].Version < s.list[j].Version
}
// Reverse reverses the list of releases sorted by the sort func.
func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) {
sortFn(list)
for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
list[i], list[j] = list[j], list[i]
}
}
// SortByName returns the list of releases sorted
// in lexicographical order.
func SortByName(list []*rspb.Release) {
sort.Sort(ByName{list})
}
// SortByDate returns the list of releases sorted by a
// release's last deployed time (in seconds).
func SortByDate(list []*rspb.Release) {
sort.Sort(ByDate{list})
}
// SortByRevision returns the list of releases sorted by a
// release's revision number (release.Version).
func SortByRevision(list []*rspb.Release) {
sort.Sort(ByRevision{list})
}

View File

@@ -48,6 +48,7 @@ type Entry struct {
KeyFile string `json:"keyFile"`
CAFile string `json:"caFile"`
InsecureSkipTLSverify bool `json:"insecure_skip_tls_verify"`
PassCredentialsAll bool `json:"pass_credentials_all"`
}
// ChartRepository represents a chart repository
@@ -82,6 +83,8 @@ func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository,
// Load loads a directory of charts as if it were a repository.
//
// It requires the presence of an index.yaml file in the directory.
//
// Deprecated: remove in Helm 4.
func (r *ChartRepository) Load() error {
dirInfo, err := os.Stat(r.Config.Name)
if err != nil {
@@ -99,7 +102,7 @@ func (r *ChartRepository) Load() error {
if strings.Contains(f.Name(), "-index.yaml") {
i, err := LoadIndexFile(path)
if err != nil {
return nil
return err
}
r.IndexFile = i
} else if strings.HasSuffix(f.Name(), ".tgz") {
@@ -127,6 +130,7 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) {
getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSverify),
getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile),
getter.WithBasicAuth(r.Config.Username, r.Config.Password),
getter.WithPassCredentialsAll(r.Config.PassCredentialsAll),
)
if err != nil {
return "", err
@@ -137,7 +141,7 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) {
return "", err
}
indexFile, err := loadIndex(index)
indexFile, err := loadIndex(index, r.Config.URL)
if err != nil {
return "", err
}
@@ -187,7 +191,9 @@ func (r *ChartRepository) generateIndex() error {
}
if !r.IndexFile.Has(ch.Name(), ch.Metadata.Version) {
r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest)
if err := r.IndexFile.MustAdd(ch.Metadata, path, r.Config.URL, digest); err != nil {
return errors.Wrapf(err, "failed adding to %s to index", path)
}
}
// TODO: If a chart exists, but has a different Digest, should we error?
}
@@ -213,6 +219,15 @@ func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion
// but it also receives credentials and TLS verify flag for the chart repository.
// TODO Helm 4, FindChartInAuthAndTLSRepoURL should be integrated into FindChartInAuthRepoURL.
func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify bool, getters getter.Providers) (string, error) {
return FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, false, false, getters)
}
// FindChartInAuthAndTLSAndPassRepoURL finds chart in chart repository pointed by repoURL
// without adding repo to repositories, like FindChartInRepoURL,
// but it also receives credentials, TLS verify flag, and if credentials should
// be passed on to other domains.
// TODO Helm 4, FindChartInAuthAndTLSAndPassRepoURL should be integrated into FindChartInAuthRepoURL.
func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify, passCredentialsAll bool, getters getter.Providers) (string, error) {
// Download and write the index file to a temporary location
buf := make([]byte, 20)
@@ -223,6 +238,7 @@ func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartV
URL: repoURL,
Username: username,
Password: password,
PassCredentialsAll: passCredentialsAll,
CertFile: certFile,
KeyFile: keyFile,
CAFile: caFile,
@@ -270,7 +286,8 @@ func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartV
// 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)
// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
parsedBaseURL, err := url.Parse(strings.TrimSuffix(baseURL, "/") + "/")
if err != nil {
return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL)
}
@@ -280,8 +297,6 @@ func ResolveReferenceURL(baseURL, refURL string) (string, error) {
return "", errors.Wrapf(err, "failed to parse %s as URL", refURL)
}
// We need a trailing slash for ResolveReference to work, but make sure there isn't already one
parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/"
return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil
}

View File

@@ -19,6 +19,7 @@ package repo
import (
"bytes"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
@@ -105,16 +106,27 @@ func LoadIndexFile(path string) (*IndexFile, error) {
if err != nil {
return nil, err
}
return loadIndex(b)
i, err := loadIndex(b, path)
if err != nil {
return nil, errors.Wrapf(err, "error loading %s", path)
}
return i, nil
}
// Add adds a file to the index
// MustAdd 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) {
func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) error {
if md.APIVersion == "" {
md.APIVersion = chart.APIVersionV1
}
if err := md.Validate(); err != nil {
return errors.Wrapf(err, "validate failed for %s", filename)
}
u := filename
if baseURL != "" {
var err error
_, file := filepath.Split(filename)
var err error
u, err = urlutil.URLJoin(baseURL, file)
if err != nil {
u = path.Join(baseURL, file)
@@ -126,10 +138,17 @@ func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
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)
ee := i.Entries[md.Name]
i.Entries[md.Name] = append(ee, cr)
return nil
}
// Add adds a file to the index and logs an error.
//
// Deprecated: Use index.MustAdd instead.
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
if err := i.MustAdd(md, filename, baseURL, digest); err != nil {
log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err)
}
}
@@ -248,7 +267,7 @@ type ChartVersion struct {
// YAML parser enabled, this field must be present.
TillerVersionDeprecated string `json:"tillerVersion,omitempty"`
// URLDeprecated is deprectaed in Helm 3, superseded by URLs. It is ignored. However,
// URLDeprecated is deprecated in Helm 3, superseded by URLs. It is ignored. However,
// with a strict YAML parser enabled, this must be present on the struct.
URLDeprecated string `json:"url,omitempty"`
}
@@ -294,19 +313,34 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
if err != nil {
return index, err
}
index.Add(c.Metadata, fname, parentURL, hash)
if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil {
return index, errors.Wrapf(err, "failed adding to %s to index", fname)
}
}
return index, nil
}
// loadIndex loads an index file and does minimal validity checking.
//
// The source parameter is only used for logging.
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func loadIndex(data []byte) (*IndexFile, error) {
func loadIndex(data []byte, source string) (*IndexFile, error) {
i := &IndexFile{}
if err := yaml.UnmarshalStrict(data, i); err != nil {
return i, err
}
for name, cvs := range i.Entries {
for idx := len(cvs) - 1; idx >= 0; idx-- {
if cvs[idx].APIVersion == "" {
cvs[idx].APIVersion = chart.APIVersionV1
}
if err := cvs[idx].Validate(); err != nil {
log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err)
cvs = append(cvs[:idx], cvs[idx+1:]...)
}
}
}
i.SortEntries()
if i.APIVersion == "" {
return i, ErrNoAPIVersion

View File

@@ -0,0 +1,257 @@
/*
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 driver // import "helm.sh/helm/v3/pkg/storage/driver"
import (
"context"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kblabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/validation"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
rspb "helm.sh/helm/v3/pkg/release"
)
var _ Driver = (*ConfigMaps)(nil)
// ConfigMapsDriverName is the string name of the driver.
const ConfigMapsDriverName = "ConfigMap"
// ConfigMaps is a wrapper around an implementation of a kubernetes
// ConfigMapsInterface.
type ConfigMaps struct {
impl corev1.ConfigMapInterface
Log func(string, ...interface{})
}
// NewConfigMaps initializes a new ConfigMaps wrapping an implementation of
// the kubernetes ConfigMapsInterface.
func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps {
return &ConfigMaps{
impl: impl,
Log: func(_ string, _ ...interface{}) {},
}
}
// Name returns the name of the driver.
func (cfgmaps *ConfigMaps) Name() string {
return ConfigMapsDriverName
}
// Get fetches the release named by key. The corresponding release is returned
// or error if not found.
func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) {
// fetch the configmap holding the release named by key
obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrReleaseNotFound
}
cfgmaps.Log("get: failed to get %q: %s", key, err)
return nil, err
}
// found the configmap, decode the base64 data string
r, err := decodeRelease(obj.Data["release"])
if err != nil {
cfgmaps.Log("get: failed to decode data %q: %s", key, err)
return nil, err
}
// return the release object
return r, nil
}
// List fetches all releases and returns the list releases such
// that filter(release) == true. An error is returned if the
// configmap fails to retrieve the releases.
func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
lsel := kblabels.Set{"owner": "helm"}.AsSelector()
opts := metav1.ListOptions{LabelSelector: lsel.String()}
list, err := cfgmaps.impl.List(context.Background(), opts)
if err != nil {
cfgmaps.Log("list: failed to list: %s", err)
return nil, err
}
var results []*rspb.Release
// iterate over the configmaps object list
// and decode each release
for _, item := range list.Items {
rls, err := decodeRelease(item.Data["release"])
if err != nil {
cfgmaps.Log("list: failed to decode release: %v: %s", item, err)
continue
}
rls.Labels = item.ObjectMeta.Labels
if filter(rls) {
results = append(results, rls)
}
}
return results, nil
}
// Query fetches all releases that match the provided map of labels.
// An error is returned if the configmap fails to retrieve the releases.
func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, error) {
ls := kblabels.Set{}
for k, v := range labels {
if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
return nil, errors.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; "))
}
ls[k] = v
}
opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()}
list, err := cfgmaps.impl.List(context.Background(), opts)
if err != nil {
cfgmaps.Log("query: failed to query with labels: %s", err)
return nil, err
}
if len(list.Items) == 0 {
return nil, ErrReleaseNotFound
}
var results []*rspb.Release
for _, item := range list.Items {
rls, err := decodeRelease(item.Data["release"])
if err != nil {
cfgmaps.Log("query: failed to decode release: %s", err)
continue
}
results = append(results, rls)
}
return results, nil
}
// Create creates a new ConfigMap holding the release. If the
// ConfigMap already exists, ErrReleaseExists is returned.
func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error {
// set labels for configmaps object meta data
var lbs labels
lbs.init()
lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix())))
// create a new configmap to hold the release
obj, err := newConfigMapsObject(key, rls, lbs)
if err != nil {
cfgmaps.Log("create: failed to encode release %q: %s", rls.Name, err)
return err
}
// push the configmap object out into the kubiverse
if _, err := cfgmaps.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil {
if apierrors.IsAlreadyExists(err) {
return ErrReleaseExists
}
cfgmaps.Log("create: failed to create: %s", err)
return err
}
return nil
}
// Update updates the ConfigMap holding the release. If not found
// the ConfigMap is created to hold the release.
func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error {
// set labels for configmaps object meta data
var lbs labels
lbs.init()
lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix())))
// create a new configmap object to hold the release
obj, err := newConfigMapsObject(key, rls, lbs)
if err != nil {
cfgmaps.Log("update: failed to encode release %q: %s", rls.Name, err)
return err
}
// push the configmap object out into the kubiverse
_, err = cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{})
if err != nil {
cfgmaps.Log("update: failed to update: %s", err)
return err
}
return nil
}
// Delete deletes the ConfigMap holding the release named by key.
func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) {
// fetch the release to check existence
if rls, err = cfgmaps.Get(key); err != nil {
return nil, err
}
// delete the release
if err = cfgmaps.impl.Delete(context.Background(), key, metav1.DeleteOptions{}); err != nil {
return rls, err
}
return rls, nil
}
// newConfigMapsObject constructs a kubernetes ConfigMap object
// to store a release. Each configmap data entry is the base64
// encoded gzipped string of a release.
//
// The following labels are used within each configmap:
//
// "modifiedAt" - timestamp indicating when this configmap was last modified. (set in Update)
// "createdAt" - timestamp indicating when this configmap was created. (set in Create)
// "version" - version of the release.
// "status" - status of the release (see pkg/release/status.go for variants)
// "owner" - owner of the configmap, currently "helm".
// "name" - name of the release.
//
func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigMap, error) {
const owner = "helm"
// encode the release
s, err := encodeRelease(rls)
if err != nil {
return nil, err
}
if lbs == nil {
lbs.init()
}
// apply labels
lbs.set("name", rls.Name)
lbs.set("owner", owner)
lbs.set("status", rls.Info.Status.String())
lbs.set("version", strconv.Itoa(rls.Version))
// create and return configmap object
return &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: key,
Labels: lbs.toMap(),
},
Data: map[string]string{"release": s},
}, nil
}

View File

@@ -0,0 +1,105 @@
/*
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 driver // import "helm.sh/helm/v3/pkg/storage/driver"
import (
"fmt"
"github.com/pkg/errors"
rspb "helm.sh/helm/v3/pkg/release"
)
var (
// ErrReleaseNotFound indicates that a release is not found.
ErrReleaseNotFound = errors.New("release: not found")
// ErrReleaseExists indicates that a release already exists.
ErrReleaseExists = errors.New("release: already exists")
// ErrInvalidKey indicates that a release key could not be parsed.
ErrInvalidKey = errors.New("release: invalid key")
// ErrNoDeployedReleases indicates that there are no releases with the given key in the deployed state
ErrNoDeployedReleases = errors.New("has no deployed releases")
)
// StorageDriverError records an error and the release name that caused it
type StorageDriverError struct {
ReleaseName string
Err error
}
func (e *StorageDriverError) Error() string {
return fmt.Sprintf("%q %s", e.ReleaseName, e.Err.Error())
}
func (e *StorageDriverError) Unwrap() error { return e.Err }
func NewErrNoDeployedReleases(releaseName string) error {
return &StorageDriverError{
ReleaseName: releaseName,
Err: ErrNoDeployedReleases,
}
}
// Creator is the interface that wraps the Create method.
//
// Create stores the release or returns ErrReleaseExists
// if an identical release already exists.
type Creator interface {
Create(key string, rls *rspb.Release) error
}
// Updator is the interface that wraps the Update method.
//
// Update updates an existing release or returns
// ErrReleaseNotFound if the release does not exist.
type Updator interface {
Update(key string, rls *rspb.Release) error
}
// Deletor is the interface that wraps the Delete method.
//
// Delete deletes the release named by key or returns
// ErrReleaseNotFound if the release does not exist.
type Deletor interface {
Delete(key string) (*rspb.Release, error)
}
// Queryor is the interface that wraps the Get and List methods.
//
// Get returns the release named by key or returns ErrReleaseNotFound
// if the release does not exist.
//
// List returns the set of all releases that satisfy the filter predicate.
//
// Query returns the set of all releases that match the provided label set.
type Queryor interface {
Get(key string) (*rspb.Release, error)
List(filter func(*rspb.Release) bool) ([]*rspb.Release, error)
Query(labels map[string]string) ([]*rspb.Release, error)
}
// Driver is the interface composed of Creator, Updator, Deletor, and Queryor
// interfaces. It defines the behavior for storing, updating, deleted,
// and retrieving Helm releases from some underlying storage mechanism,
// e.g. memory, configmaps.
type Driver interface {
Creator
Updator
Deletor
Queryor
Name() string
}

View File

@@ -0,0 +1,48 @@
/*
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 driver
// labels is a map of key value pairs to be included as metadata in a configmap object.
type labels map[string]string
func (lbs *labels) init() { *lbs = labels(make(map[string]string)) }
func (lbs labels) get(key string) string { return lbs[key] }
func (lbs labels) set(key, val string) { lbs[key] = val }
func (lbs labels) keys() (ls []string) {
for key := range lbs {
ls = append(ls, key)
}
return
}
func (lbs labels) match(set labels) bool {
for _, key := range set.keys() {
if lbs.get(key) != set.get(key) {
return false
}
}
return true
}
func (lbs labels) toMap() map[string]string { return lbs }
func (lbs *labels) fromMap(kvs map[string]string) {
for k, v := range kvs {
lbs.set(k, v)
}
}

View File

@@ -0,0 +1,240 @@
/*
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 driver
import (
"strconv"
"strings"
"sync"
rspb "helm.sh/helm/v3/pkg/release"
)
var _ Driver = (*Memory)(nil)
const (
// MemoryDriverName is the string name of this driver.
MemoryDriverName = "Memory"
defaultNamespace = "default"
)
// A map of release names to list of release records
type memReleases map[string]records
// Memory is the in-memory storage driver implementation.
type Memory struct {
sync.RWMutex
namespace string
// A map of namespaces to releases
cache map[string]memReleases
}
// NewMemory initializes a new memory driver.
func NewMemory() *Memory {
return &Memory{cache: map[string]memReleases{}, namespace: "default"}
}
// SetNamespace sets a specific namespace in which releases will be accessed.
// An empty string indicates all namespaces (for the list operation)
func (mem *Memory) SetNamespace(ns string) {
mem.namespace = ns
}
// Name returns the name of the driver.
func (mem *Memory) Name() string {
return MemoryDriverName
}
// Get returns the release named by key or returns ErrReleaseNotFound.
func (mem *Memory) Get(key string) (*rspb.Release, error) {
defer unlock(mem.rlock())
keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.")
switch elems := strings.Split(keyWithoutPrefix, ".v"); len(elems) {
case 2:
name, ver := elems[0], elems[1]
if _, err := strconv.Atoi(ver); err != nil {
return nil, ErrInvalidKey
}
if recs, ok := mem.cache[mem.namespace][name]; ok {
if r := recs.Get(key); r != nil {
return r.rls, nil
}
}
return nil, ErrReleaseNotFound
default:
return nil, ErrInvalidKey
}
}
// List returns the list of all releases such that filter(release) == true
func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
defer unlock(mem.rlock())
var ls []*rspb.Release
for namespace := range mem.cache {
if mem.namespace != "" {
// Should only list releases of this namespace
namespace = mem.namespace
}
for _, recs := range mem.cache[namespace] {
recs.Iter(func(_ int, rec *record) bool {
if filter(rec.rls) {
ls = append(ls, rec.rls)
}
return true
})
}
if mem.namespace != "" {
// Should only list releases of this namespace
break
}
}
return ls, nil
}
// Query returns the set of releases that match the provided set of labels
func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) {
defer unlock(mem.rlock())
var lbs labels
lbs.init()
lbs.fromMap(keyvals)
var ls []*rspb.Release
for namespace := range mem.cache {
if mem.namespace != "" {
// Should only query releases of this namespace
namespace = mem.namespace
}
for _, recs := range mem.cache[namespace] {
recs.Iter(func(_ int, rec *record) bool {
// A query for a release name that doesn't exist (has been deleted)
// can cause rec to be nil.
if rec == nil {
return false
}
if rec.lbs.match(lbs) {
ls = append(ls, rec.rls)
}
return true
})
}
if mem.namespace != "" {
// Should only query releases of this namespace
break
}
}
if len(ls) == 0 {
return nil, ErrReleaseNotFound
}
return ls, nil
}
// Create creates a new release or returns ErrReleaseExists.
func (mem *Memory) Create(key string, rls *rspb.Release) error {
defer unlock(mem.wlock())
// For backwards compatibility, we protect against an unset namespace
namespace := rls.Namespace
if namespace == "" {
namespace = defaultNamespace
}
mem.SetNamespace(namespace)
if _, ok := mem.cache[namespace]; !ok {
mem.cache[namespace] = memReleases{}
}
if recs, ok := mem.cache[namespace][rls.Name]; ok {
if err := recs.Add(newRecord(key, rls)); err != nil {
return err
}
mem.cache[namespace][rls.Name] = recs
return nil
}
mem.cache[namespace][rls.Name] = records{newRecord(key, rls)}
return nil
}
// Update updates a release or returns ErrReleaseNotFound.
func (mem *Memory) Update(key string, rls *rspb.Release) error {
defer unlock(mem.wlock())
// For backwards compatibility, we protect against an unset namespace
namespace := rls.Namespace
if namespace == "" {
namespace = defaultNamespace
}
mem.SetNamespace(namespace)
if _, ok := mem.cache[namespace]; ok {
if rs, ok := mem.cache[namespace][rls.Name]; ok && rs.Exists(key) {
rs.Replace(key, newRecord(key, rls))
return nil
}
}
return ErrReleaseNotFound
}
// Delete deletes a release or returns ErrReleaseNotFound.
func (mem *Memory) Delete(key string) (*rspb.Release, error) {
defer unlock(mem.wlock())
keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.")
elems := strings.Split(keyWithoutPrefix, ".v")
if len(elems) != 2 {
return nil, ErrInvalidKey
}
name, ver := elems[0], elems[1]
if _, err := strconv.Atoi(ver); err != nil {
return nil, ErrInvalidKey
}
if _, ok := mem.cache[mem.namespace]; ok {
if recs, ok := mem.cache[mem.namespace][name]; ok {
if r := recs.Remove(key); r != nil {
// recs.Remove changes the slice reference, so we have to re-assign it.
mem.cache[mem.namespace][name] = recs
return r.rls, nil
}
}
}
return nil, ErrReleaseNotFound
}
// wlock locks mem for writing
func (mem *Memory) wlock() func() {
mem.Lock()
return func() { mem.Unlock() }
}
// rlock locks mem for reading
func (mem *Memory) rlock() func() {
mem.RLock()
return func() { mem.RUnlock() }
}
// unlock calls fn which reverses a mem.rlock or mem.wlock. e.g:
// ```defer unlock(mem.rlock())```, locks mem for reading at the
// call point of defer and unlocks upon exiting the block.
func unlock(fn func()) { fn() }

View File

@@ -0,0 +1,124 @@
/*
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 driver // import "helm.sh/helm/v3/pkg/storage/driver"
import (
"sort"
"strconv"
rspb "helm.sh/helm/v3/pkg/release"
)
// records holds a list of in-memory release records
type records []*record
func (rs records) Len() int { return len(rs) }
func (rs records) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] }
func (rs records) Less(i, j int) bool { return rs[i].rls.Version < rs[j].rls.Version }
func (rs *records) Add(r *record) error {
if r == nil {
return nil
}
if rs.Exists(r.key) {
return ErrReleaseExists
}
*rs = append(*rs, r)
sort.Sort(*rs)
return nil
}
func (rs records) Get(key string) *record {
if i, ok := rs.Index(key); ok {
return rs[i]
}
return nil
}
func (rs *records) Iter(fn func(int, *record) bool) {
cp := make([]*record, len(*rs))
copy(cp, *rs)
for i, r := range cp {
if !fn(i, r) {
return
}
}
}
func (rs *records) Index(key string) (int, bool) {
for i, r := range *rs {
if r.key == key {
return i, true
}
}
return -1, false
}
func (rs records) Exists(key string) bool {
_, ok := rs.Index(key)
return ok
}
func (rs *records) Remove(key string) (r *record) {
if i, ok := rs.Index(key); ok {
return rs.removeAt(i)
}
return nil
}
func (rs *records) Replace(key string, rec *record) *record {
if i, ok := rs.Index(key); ok {
old := (*rs)[i]
(*rs)[i] = rec
return old
}
return nil
}
func (rs *records) removeAt(index int) *record {
r := (*rs)[index]
(*rs)[index] = nil
copy((*rs)[index:], (*rs)[index+1:])
*rs = (*rs)[:len(*rs)-1]
return r
}
// record is the data structure used to cache releases
// for the in-memory storage driver
type record struct {
key string
lbs labels
rls *rspb.Release
}
// newRecord creates a new in-memory release record
func newRecord(key string, rls *rspb.Release) *record {
var lbs labels
lbs.init()
lbs.set("name", rls.Name)
lbs.set("owner", "helm")
lbs.set("status", rls.Info.Status.String())
lbs.set("version", strconv.Itoa(rls.Version))
// return &record{key: key, lbs: lbs, rls: proto.Clone(rls).(*rspb.Release)}
return &record{key: key, lbs: lbs, rls: rls}
}

View File

@@ -0,0 +1,250 @@
/*
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 driver // import "helm.sh/helm/v3/pkg/storage/driver"
import (
"context"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
kblabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/validation"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
rspb "helm.sh/helm/v3/pkg/release"
)
var _ Driver = (*Secrets)(nil)
// SecretsDriverName is the string name of the driver.
const SecretsDriverName = "Secret"
// Secrets is a wrapper around an implementation of a kubernetes
// SecretsInterface.
type Secrets struct {
impl corev1.SecretInterface
Log func(string, ...interface{})
}
// NewSecrets initializes a new Secrets wrapping an implementation of
// the kubernetes SecretsInterface.
func NewSecrets(impl corev1.SecretInterface) *Secrets {
return &Secrets{
impl: impl,
Log: func(_ string, _ ...interface{}) {},
}
}
// Name returns the name of the driver.
func (secrets *Secrets) Name() string {
return SecretsDriverName
}
// Get fetches the release named by key. The corresponding release is returned
// or error if not found.
func (secrets *Secrets) Get(key string) (*rspb.Release, error) {
// fetch the secret holding the release named by key
obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrReleaseNotFound
}
return nil, errors.Wrapf(err, "get: failed to get %q", key)
}
// found the secret, decode the base64 data string
r, err := decodeRelease(string(obj.Data["release"]))
return r, errors.Wrapf(err, "get: failed to decode data %q", key)
}
// List fetches all releases and returns the list releases such
// that filter(release) == true. An error is returned if the
// secret fails to retrieve the releases.
func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
lsel := kblabels.Set{"owner": "helm"}.AsSelector()
opts := metav1.ListOptions{LabelSelector: lsel.String()}
list, err := secrets.impl.List(context.Background(), opts)
if err != nil {
return nil, errors.Wrap(err, "list: failed to list")
}
var results []*rspb.Release
// iterate over the secrets object list
// and decode each release
for _, item := range list.Items {
rls, err := decodeRelease(string(item.Data["release"]))
if err != nil {
secrets.Log("list: failed to decode release: %v: %s", item, err)
continue
}
rls.Labels = item.ObjectMeta.Labels
if filter(rls) {
results = append(results, rls)
}
}
return results, nil
}
// Query fetches all releases that match the provided map of labels.
// An error is returned if the secret fails to retrieve the releases.
func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) {
ls := kblabels.Set{}
for k, v := range labels {
if errs := validation.IsValidLabelValue(v); len(errs) != 0 {
return nil, errors.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; "))
}
ls[k] = v
}
opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()}
list, err := secrets.impl.List(context.Background(), opts)
if err != nil {
return nil, errors.Wrap(err, "query: failed to query with labels")
}
if len(list.Items) == 0 {
return nil, ErrReleaseNotFound
}
var results []*rspb.Release
for _, item := range list.Items {
rls, err := decodeRelease(string(item.Data["release"]))
if err != nil {
secrets.Log("query: failed to decode release: %s", err)
continue
}
results = append(results, rls)
}
return results, nil
}
// Create creates a new Secret holding the release. If the
// Secret already exists, ErrReleaseExists is returned.
func (secrets *Secrets) Create(key string, rls *rspb.Release) error {
// set labels for secrets object meta data
var lbs labels
lbs.init()
lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix())))
// create a new secret to hold the release
obj, err := newSecretsObject(key, rls, lbs)
if err != nil {
return errors.Wrapf(err, "create: failed to encode release %q", rls.Name)
}
// push the secret object out into the kubiverse
if _, err := secrets.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil {
if apierrors.IsAlreadyExists(err) {
return ErrReleaseExists
}
return errors.Wrap(err, "create: failed to create")
}
return nil
}
// Update updates the Secret holding the release. If not found
// the Secret is created to hold the release.
func (secrets *Secrets) Update(key string, rls *rspb.Release) error {
// set labels for secrets object meta data
var lbs labels
lbs.init()
lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix())))
// create a new secret object to hold the release
obj, err := newSecretsObject(key, rls, lbs)
if err != nil {
return errors.Wrapf(err, "update: failed to encode release %q", rls.Name)
}
// push the secret object out into the kubiverse
_, err = secrets.impl.Update(context.Background(), obj, metav1.UpdateOptions{})
return errors.Wrap(err, "update: failed to update")
}
// Delete deletes the Secret holding the release named by key.
func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) {
// fetch the release to check existence
if rls, err = secrets.Get(key); err != nil {
return nil, err
}
// delete the release
err = secrets.impl.Delete(context.Background(), key, metav1.DeleteOptions{})
return rls, err
}
// newSecretsObject constructs a kubernetes Secret object
// to store a release. Each secret data entry is the base64
// encoded gzipped string of a release.
//
// The following labels are used within each secret:
//
// "modifiedAt" - timestamp indicating when this secret was last modified. (set in Update)
// "createdAt" - timestamp indicating when this secret was created. (set in Create)
// "version" - version of the release.
// "status" - status of the release (see pkg/release/status.go for variants)
// "owner" - owner of the secret, currently "helm".
// "name" - name of the release.
//
func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, error) {
const owner = "helm"
// encode the release
s, err := encodeRelease(rls)
if err != nil {
return nil, err
}
if lbs == nil {
lbs.init()
}
// apply labels
lbs.set("name", rls.Name)
lbs.set("owner", owner)
lbs.set("status", rls.Info.Status.String())
lbs.set("version", strconv.Itoa(rls.Version))
// create and return secret object.
// Helm 3 introduced setting the 'Type' field
// in the Kubernetes storage object.
// Helm defines the field content as follows:
// <helm_domain>/<helm_object>.v<helm_object_version>
// Type field for Helm 3: helm.sh/release.v1
// Note: Version starts at 'v1' for Helm 3 and
// should be incremented if the release object
// metadata is modified.
// This would potentially be a breaking change
// and should only happen between major versions.
return &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: key,
Labels: lbs.toMap(),
},
Type: "helm.sh/release.v1",
Data: map[string][]byte{"release": []byte(s)},
}, nil
}

View File

@@ -0,0 +1,496 @@
/*
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 driver // import "helm.sh/helm/v3/pkg/storage/driver"
import (
"fmt"
"sort"
"time"
"github.com/jmoiron/sqlx"
migrate "github.com/rubenv/sql-migrate"
sq "github.com/Masterminds/squirrel"
// Import pq for postgres dialect
_ "github.com/lib/pq"
rspb "helm.sh/helm/v3/pkg/release"
)
var _ Driver = (*SQL)(nil)
var labelMap = map[string]struct{}{
"modifiedAt": {},
"createdAt": {},
"version": {},
"status": {},
"owner": {},
"name": {},
}
const postgreSQLDialect = "postgres"
// SQLDriverName is the string name of this driver.
const SQLDriverName = "SQL"
const sqlReleaseTableName = "releases_v1"
const (
sqlReleaseTableKeyColumn = "key"
sqlReleaseTableTypeColumn = "type"
sqlReleaseTableBodyColumn = "body"
sqlReleaseTableNameColumn = "name"
sqlReleaseTableNamespaceColumn = "namespace"
sqlReleaseTableVersionColumn = "version"
sqlReleaseTableStatusColumn = "status"
sqlReleaseTableOwnerColumn = "owner"
sqlReleaseTableCreatedAtColumn = "createdAt"
sqlReleaseTableModifiedAtColumn = "modifiedAt"
)
const (
sqlReleaseDefaultOwner = "helm"
sqlReleaseDefaultType = "helm.sh/release.v1"
)
// SQL is the sql storage driver implementation.
type SQL struct {
db *sqlx.DB
namespace string
statementBuilder sq.StatementBuilderType
Log func(string, ...interface{})
}
// Name returns the name of the driver.
func (s *SQL) Name() string {
return SQLDriverName
}
func (s *SQL) ensureDBSetup() error {
// Populate the database with the relations we need if they don't exist yet
migrations := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "init",
Up: []string{
fmt.Sprintf(`
CREATE TABLE %s (
%s VARCHAR(67),
%s VARCHAR(64) NOT NULL,
%s TEXT NOT NULL,
%s VARCHAR(64) NOT NULL,
%s VARCHAR(64) NOT NULL,
%s INTEGER NOT NULL,
%s TEXT NOT NULL,
%s TEXT NOT NULL,
%s INTEGER NOT NULL,
%s INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY(%s, %s)
);
CREATE INDEX ON %s (%s, %s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
GRANT ALL ON %s TO PUBLIC;
ALTER TABLE %s ENABLE ROW LEVEL SECURITY;
`,
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableTypeColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableNameColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableVersionColumn,
sqlReleaseTableStatusColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableCreatedAtColumn,
sqlReleaseTableModifiedAtColumn,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableName,
sqlReleaseTableKeyColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableName,
sqlReleaseTableVersionColumn,
sqlReleaseTableName,
sqlReleaseTableStatusColumn,
sqlReleaseTableName,
sqlReleaseTableOwnerColumn,
sqlReleaseTableName,
sqlReleaseTableCreatedAtColumn,
sqlReleaseTableName,
sqlReleaseTableModifiedAtColumn,
sqlReleaseTableName,
sqlReleaseTableName,
),
},
Down: []string{
fmt.Sprintf(`
DROP TABLE %s;
`, sqlReleaseTableName),
},
},
},
}
_, err := migrate.Exec(s.db.DB, postgreSQLDialect, migrations, migrate.Up)
return err
}
// SQLReleaseWrapper describes how Helm releases are stored in an SQL database
type SQLReleaseWrapper struct {
// The primary key, made of {release-name}.{release-version}
Key string `db:"key"`
// See https://github.com/helm/helm/blob/c9fe3d118caec699eb2565df9838673af379ce12/pkg/storage/driver/secrets.go#L231
Type string `db:"type"`
// The rspb.Release body, as a base64-encoded string
Body string `db:"body"`
// Release "labels" that can be used as filters in the storage.Query(labels map[string]string)
// we implemented. Note that allowing Helm users to filter against new dimensions will require a
// new migration to be added, and the Create and/or update functions to be updated accordingly.
Name string `db:"name"`
Namespace string `db:"namespace"`
Version int `db:"version"`
Status string `db:"status"`
Owner string `db:"owner"`
CreatedAt int `db:"createdAt"`
ModifiedAt int `db:"modifiedAt"`
}
// NewSQL initializes a new sql driver.
func NewSQL(connectionString string, logger func(string, ...interface{}), namespace string) (*SQL, error) {
db, err := sqlx.Connect(postgreSQLDialect, connectionString)
if err != nil {
return nil, err
}
driver := &SQL{
db: db,
Log: logger,
statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar),
}
if err := driver.ensureDBSetup(); err != nil {
return nil, err
}
driver.namespace = namespace
return driver, nil
}
// Get returns the release named by key.
func (s *SQL) Get(key string) (*rspb.Release, error) {
var record SQLReleaseWrapper
qb := s.statementBuilder.
Select(sqlReleaseTableBodyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace})
query, args, err := qb.ToSql()
if err != nil {
s.Log("failed to build query: %v", err)
return nil, err
}
// Get will return an error if the result is empty
if err := s.db.Get(&record, query, args...); err != nil {
s.Log("got SQL error when getting release %s: %v", key, err)
return nil, ErrReleaseNotFound
}
release, err := decodeRelease(record.Body)
if err != nil {
s.Log("get: failed to decode data %q: %v", key, err)
return nil, err
}
return release, nil
}
// List returns the list of all releases such that filter(release) == true
func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
sb := s.statementBuilder.
Select(sqlReleaseTableBodyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableOwnerColumn: sqlReleaseDefaultOwner})
// If a namespace was specified, we only list releases from that namespace
if s.namespace != "" {
sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace})
}
query, args, err := sb.ToSql()
if err != nil {
s.Log("failed to build query: %v", err)
return nil, err
}
var records = []SQLReleaseWrapper{}
if err := s.db.Select(&records, query, args...); err != nil {
s.Log("list: failed to list: %v", err)
return nil, err
}
var releases []*rspb.Release
for _, record := range records {
release, err := decodeRelease(record.Body)
if err != nil {
s.Log("list: failed to decode release: %v: %v", record, err)
continue
}
if filter(release) {
releases = append(releases, release)
}
}
return releases, nil
}
// Query returns the set of releases that match the provided set of labels.
func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) {
sb := s.statementBuilder.
Select(sqlReleaseTableBodyColumn).
From(sqlReleaseTableName)
keys := make([]string, 0, len(labels))
for key := range labels {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if _, ok := labelMap[key]; ok {
sb = sb.Where(sq.Eq{key: labels[key]})
} else {
s.Log("unknown label %s", key)
return nil, fmt.Errorf("unknown label %s", key)
}
}
// If a namespace was specified, we only list releases from that namespace
if s.namespace != "" {
sb = sb.Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace})
}
// Build our query
query, args, err := sb.ToSql()
if err != nil {
s.Log("failed to build query: %v", err)
return nil, err
}
var records = []SQLReleaseWrapper{}
if err := s.db.Select(&records, query, args...); err != nil {
s.Log("list: failed to query with labels: %v", err)
return nil, err
}
if len(records) == 0 {
return nil, ErrReleaseNotFound
}
var releases []*rspb.Release
for _, record := range records {
release, err := decodeRelease(record.Body)
if err != nil {
s.Log("list: failed to decode release: %v: %v", record, err)
continue
}
releases = append(releases, release)
}
if len(releases) == 0 {
return nil, ErrReleaseNotFound
}
return releases, nil
}
// Create creates a new release.
func (s *SQL) Create(key string, rls *rspb.Release) error {
namespace := rls.Namespace
if namespace == "" {
namespace = defaultNamespace
}
s.namespace = namespace
body, err := encodeRelease(rls)
if err != nil {
s.Log("failed to encode release: %v", err)
return err
}
transaction, err := s.db.Beginx()
if err != nil {
s.Log("failed to start SQL transaction: %v", err)
return fmt.Errorf("error beginning transaction: %v", err)
}
insertQuery, args, err := s.statementBuilder.
Insert(sqlReleaseTableName).
Columns(
sqlReleaseTableKeyColumn,
sqlReleaseTableTypeColumn,
sqlReleaseTableBodyColumn,
sqlReleaseTableNameColumn,
sqlReleaseTableNamespaceColumn,
sqlReleaseTableVersionColumn,
sqlReleaseTableStatusColumn,
sqlReleaseTableOwnerColumn,
sqlReleaseTableCreatedAtColumn,
).
Values(
key,
sqlReleaseDefaultType,
body,
rls.Name,
namespace,
int(rls.Version),
rls.Info.Status.String(),
sqlReleaseDefaultOwner,
int(time.Now().Unix()),
).ToSql()
if err != nil {
s.Log("failed to build insert query: %v", err)
return err
}
if _, err := transaction.Exec(insertQuery, args...); err != nil {
defer transaction.Rollback()
selectQuery, args, buildErr := s.statementBuilder.
Select(sqlReleaseTableKeyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}).
ToSql()
if buildErr != nil {
s.Log("failed to build select query: %v", buildErr)
return err
}
var record SQLReleaseWrapper
if err := transaction.Get(&record, selectQuery, args...); err == nil {
s.Log("release %s already exists", key)
return ErrReleaseExists
}
s.Log("failed to store release %s in SQL database: %v", key, err)
return err
}
defer transaction.Commit()
return nil
}
// Update updates a release.
func (s *SQL) Update(key string, rls *rspb.Release) error {
namespace := rls.Namespace
if namespace == "" {
namespace = defaultNamespace
}
s.namespace = namespace
body, err := encodeRelease(rls)
if err != nil {
s.Log("failed to encode release: %v", err)
return err
}
query, args, err := s.statementBuilder.
Update(sqlReleaseTableName).
Set(sqlReleaseTableBodyColumn, body).
Set(sqlReleaseTableNameColumn, rls.Name).
Set(sqlReleaseTableVersionColumn, int(rls.Version)).
Set(sqlReleaseTableStatusColumn, rls.Info.Status.String()).
Set(sqlReleaseTableOwnerColumn, sqlReleaseDefaultOwner).
Set(sqlReleaseTableModifiedAtColumn, int(time.Now().Unix())).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: namespace}).
ToSql()
if err != nil {
s.Log("failed to build update query: %v", err)
return err
}
if _, err := s.db.Exec(query, args...); err != nil {
s.Log("failed to update release %s in SQL database: %v", key, err)
return err
}
return nil
}
// Delete deletes a release or returns ErrReleaseNotFound.
func (s *SQL) Delete(key string) (*rspb.Release, error) {
transaction, err := s.db.Beginx()
if err != nil {
s.Log("failed to start SQL transaction: %v", err)
return nil, fmt.Errorf("error beginning transaction: %v", err)
}
selectQuery, args, err := s.statementBuilder.
Select(sqlReleaseTableBodyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}).
ToSql()
if err != nil {
s.Log("failed to build select query: %v", err)
return nil, err
}
var record SQLReleaseWrapper
err = transaction.Get(&record, selectQuery, args...)
if err != nil {
s.Log("release %s not found: %v", key, err)
return nil, ErrReleaseNotFound
}
release, err := decodeRelease(record.Body)
if err != nil {
s.Log("failed to decode release %s: %v", key, err)
transaction.Rollback()
return nil, err
}
defer transaction.Commit()
deleteQuery, args, err := s.statementBuilder.
Delete(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableKeyColumn: key}).
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}).
ToSql()
if err != nil {
s.Log("failed to build select query: %v", err)
return nil, err
}
_, err = transaction.Exec(deleteQuery, args...)
return release, err
}

View File

@@ -0,0 +1,85 @@
/*
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 driver // import "helm.sh/helm/v3/pkg/storage/driver"
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/json"
"io/ioutil"
rspb "helm.sh/helm/v3/pkg/release"
)
var b64 = base64.StdEncoding
var magicGzip = []byte{0x1f, 0x8b, 0x08}
// encodeRelease encodes a release returning a base64 encoded
// gzipped string representation, or error.
func encodeRelease(rls *rspb.Release) (string, error) {
b, err := json.Marshal(rls)
if err != nil {
return "", err
}
var buf bytes.Buffer
w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if err != nil {
return "", err
}
if _, err = w.Write(b); err != nil {
return "", err
}
w.Close()
return b64.EncodeToString(buf.Bytes()), nil
}
// decodeRelease decodes the bytes of data into a release
// type. Data must contain a base64 encoded gzipped string of a
// valid release, otherwise an error is returned.
func decodeRelease(data string) (*rspb.Release, error) {
// base64 decode string
b, err := b64.DecodeString(data)
if err != nil {
return nil, err
}
// For backwards compatibility with releases that were stored before
// compression was introduced we skip decompression if the
// gzip magic header is not found
if bytes.Equal(b[0:3], magicGzip) {
r, err := gzip.NewReader(bytes.NewReader(b))
if err != nil {
return nil, err
}
defer r.Close()
b2, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
b = b2
}
var rls rspb.Release
// unmarshal release object bytes
if err := json.Unmarshal(b, &rls); err != nil {
return nil, err
}
return &rls, nil
}

View File

@@ -0,0 +1,266 @@
/*
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 storage // import "helm.sh/helm/v3/pkg/storage"
import (
"fmt"
"strings"
"github.com/pkg/errors"
rspb "helm.sh/helm/v3/pkg/release"
relutil "helm.sh/helm/v3/pkg/releaseutil"
"helm.sh/helm/v3/pkg/storage/driver"
)
// HelmStorageType is the type field of the Kubernetes storage object which stores the Helm release
// version. It is modified slightly replacing the '/': sh.helm/release.v1
// Note: The version 'v1' is incremented if the release object metadata is
// modified between major releases.
// This constant is used as a prefix for the Kubernetes storage object name.
const HelmStorageType = "sh.helm.release.v1"
// Storage represents a storage engine for a Release.
type Storage struct {
driver.Driver
// MaxHistory specifies the maximum number of historical releases that will
// be retained, including the most recent release. Values of 0 or less are
// ignored (meaning no limits are imposed).
MaxHistory int
Log func(string, ...interface{})
}
// Get retrieves the release from storage. An error is returned
// if the storage driver failed to fetch the release, or the
// release identified by the key, version pair does not exist.
func (s *Storage) Get(name string, version int) (*rspb.Release, error) {
s.Log("getting release %q", makeKey(name, version))
return s.Driver.Get(makeKey(name, version))
}
// Create creates a new storage entry holding the release. An
// error is returned if the storage driver failed to store the
// release, or a release with identical an key already exists.
func (s *Storage) Create(rls *rspb.Release) error {
s.Log("creating release %q", makeKey(rls.Name, rls.Version))
if s.MaxHistory > 0 {
// Want to make space for one more release.
if err := s.removeLeastRecent(rls.Name, s.MaxHistory-1); err != nil &&
!errors.Is(err, driver.ErrReleaseNotFound) {
return err
}
}
return s.Driver.Create(makeKey(rls.Name, rls.Version), rls)
}
// Update updates the release in storage. An error is returned if the
// storage backend fails to update the release or if the release
// does not exist.
func (s *Storage) Update(rls *rspb.Release) error {
s.Log("updating release %q", makeKey(rls.Name, rls.Version))
return s.Driver.Update(makeKey(rls.Name, rls.Version), rls)
}
// Delete deletes the release from storage. An error is returned if
// the storage backend fails to delete the release or if the release
// does not exist.
func (s *Storage) Delete(name string, version int) (*rspb.Release, error) {
s.Log("deleting release %q", makeKey(name, version))
return s.Driver.Delete(makeKey(name, version))
}
// ListReleases returns all releases from storage. An error is returned if the
// storage backend fails to retrieve the releases.
func (s *Storage) ListReleases() ([]*rspb.Release, error) {
s.Log("listing all releases in storage")
return s.Driver.List(func(_ *rspb.Release) bool { return true })
}
// ListUninstalled returns all releases with Status == UNINSTALLED. An error is returned
// if the storage backend fails to retrieve the releases.
func (s *Storage) ListUninstalled() ([]*rspb.Release, error) {
s.Log("listing uninstalled releases in storage")
return s.Driver.List(func(rls *rspb.Release) bool {
return relutil.StatusFilter(rspb.StatusUninstalled).Check(rls)
})
}
// ListDeployed returns all releases with Status == DEPLOYED. An error is returned
// if the storage backend fails to retrieve the releases.
func (s *Storage) ListDeployed() ([]*rspb.Release, error) {
s.Log("listing all deployed releases in storage")
return s.Driver.List(func(rls *rspb.Release) bool {
return relutil.StatusFilter(rspb.StatusDeployed).Check(rls)
})
}
// Deployed returns the last deployed release with the provided release name, or
// returns ErrReleaseNotFound if not found.
func (s *Storage) Deployed(name string) (*rspb.Release, error) {
ls, err := s.DeployedAll(name)
if err != nil {
return nil, err
}
if len(ls) == 0 {
return nil, driver.NewErrNoDeployedReleases(name)
}
// If executed concurrently, Helm's database gets corrupted
// and multiple releases are DEPLOYED. Take the latest.
relutil.Reverse(ls, relutil.SortByRevision)
return ls[0], nil
}
// DeployedAll returns all deployed releases with the provided name, or
// returns ErrReleaseNotFound if not found.
func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) {
s.Log("getting deployed releases from %q history", name)
ls, err := s.Driver.Query(map[string]string{
"name": name,
"owner": "helm",
"status": "deployed",
})
if err == nil {
return ls, nil
}
if strings.Contains(err.Error(), "not found") {
return nil, driver.NewErrNoDeployedReleases(name)
}
return nil, err
}
// History returns the revision history for the release with the provided name, or
// returns ErrReleaseNotFound if no such release name exists.
func (s *Storage) History(name string) ([]*rspb.Release, error) {
s.Log("getting release history for %q", name)
return s.Driver.Query(map[string]string{"name": name, "owner": "helm"})
}
// removeLeastRecent removes items from history until the length number of releases
// does not exceed max.
//
// We allow max to be set explicitly so that calling functions can "make space"
// for the new records they are going to write.
func (s *Storage) removeLeastRecent(name string, max int) error {
if max < 0 {
return nil
}
h, err := s.History(name)
if err != nil {
return err
}
if len(h) <= max {
return nil
}
// We want oldest to newest
relutil.SortByRevision(h)
lastDeployed, err := s.Deployed(name)
if err != nil {
return err
}
var toDelete []*rspb.Release
for _, rel := range h {
// once we have enough releases to delete to reach the max, stop
if len(h)-len(toDelete) == max {
break
}
if lastDeployed != nil {
if rel.Version != lastDeployed.Version {
toDelete = append(toDelete, rel)
}
} else {
toDelete = append(toDelete, rel)
}
}
// Delete as many as possible. In the case of API throughput limitations,
// multiple invocations of this function will eventually delete them all.
errs := []error{}
for _, rel := range toDelete {
err = s.deleteReleaseVersion(name, rel.Version)
if err != nil {
errs = append(errs, err)
}
}
s.Log("Pruned %d record(s) from %s with %d error(s)", len(toDelete), name, len(errs))
switch c := len(errs); c {
case 0:
return nil
case 1:
return errs[0]
default:
return errors.Errorf("encountered %d deletion errors. First is: %s", c, errs[0])
}
}
func (s *Storage) deleteReleaseVersion(name string, version int) error {
key := makeKey(name, version)
_, err := s.Delete(name, version)
if err != nil {
s.Log("error pruning %s from release history: %s", key, err)
return err
}
return nil
}
// Last fetches the last revision of the named release.
func (s *Storage) Last(name string) (*rspb.Release, error) {
s.Log("getting last revision of %q", name)
h, err := s.History(name)
if err != nil {
return nil, err
}
if len(h) == 0 {
return nil, errors.Errorf("no revision for release %q", name)
}
relutil.Reverse(h, relutil.SortByRevision)
return h[0], nil
}
// makeKey concatenates the Kubernetes storage object type, a release name and version
// into a string with format:```<helm_storage_type>.<release_name>.v<release_version>```.
// The storage type is prepended to keep name uniqueness between different
// release storage types. An example of clash when not using the type:
// https://github.com/helm/helm/issues/6435.
// This key is used to uniquely identify storage objects.
func makeKey(rlsname string, version int) string {
return fmt.Sprintf("%s.%s.v%d", HelmStorageType, rlsname, version)
}
// Init initializes a new storage backend with the driver d.
// If d is nil, the default in-memory driver is used.
func Init(d driver.Driver) *Storage {
// default driver is in memory
if d == nil {
d = driver.NewMemory()
}
return &Storage{
Driver: d,
Log: func(_ string, _ ...interface{}) {},
}
}

View File

@@ -0,0 +1,32 @@
/*
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 strvals provides tools for working with strval lines.
Helm supports a compressed format for YAML settings which we call strvals.
The format is roughly like this:
name=value,topname.subname=value
The above is equivalent to the YAML document
name: value
topname:
subname: value
This package provides a parser and utilities for converting the strvals format
to other formats.
*/
package strvals

View File

@@ -0,0 +1,446 @@
/*
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 strvals
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
)
// ErrNotList indicates that a non-list was treated as a list.
var ErrNotList = errors.New("not a list")
// ToYAML takes a string of arguments and converts to a YAML document.
func ToYAML(s string) (string, error) {
m, err := Parse(s)
if err != nil {
return "", err
}
d, err := yaml.Marshal(m)
return strings.TrimSuffix(string(d), "\n"), err
}
// Parse parses a set line.
//
// A set line is of the form name1=value1,name2=value2
func Parse(s string) (map[string]interface{}, error) {
vals := map[string]interface{}{}
scanner := bytes.NewBufferString(s)
t := newParser(scanner, vals, false)
err := t.parse()
return vals, err
}
// ParseString parses a set line and forces a string value.
//
// A set line is of the form name1=value1,name2=value2
func ParseString(s string) (map[string]interface{}, error) {
vals := map[string]interface{}{}
scanner := bytes.NewBufferString(s)
t := newParser(scanner, vals, true)
err := t.parse()
return vals, err
}
// ParseInto parses a strvals line and merges the result into dest.
//
// If the strval string has a key that exists in dest, it overwrites the
// dest version.
func ParseInto(s string, dest map[string]interface{}) error {
scanner := bytes.NewBufferString(s)
t := newParser(scanner, dest, false)
return t.parse()
}
// ParseFile parses a set line, but its final value is loaded from the file at the path specified by the original value.
//
// A set line is of the form name1=path1,name2=path2
//
// When the files at path1 and path2 contained "val1" and "val2" respectively, the set line is consumed as
// name1=val1,name2=val2
func ParseFile(s string, reader RunesValueReader) (map[string]interface{}, error) {
vals := map[string]interface{}{}
scanner := bytes.NewBufferString(s)
t := newFileParser(scanner, vals, reader)
err := t.parse()
return vals, err
}
// ParseIntoString parses a strvals line and merges the result into dest.
//
// This method always returns a string as the value.
func ParseIntoString(s string, dest map[string]interface{}) error {
scanner := bytes.NewBufferString(s)
t := newParser(scanner, dest, true)
return t.parse()
}
// ParseIntoFile parses a filevals line and merges the result into dest.
//
// This method always returns a string as the value.
func ParseIntoFile(s string, dest map[string]interface{}, reader RunesValueReader) error {
scanner := bytes.NewBufferString(s)
t := newFileParser(scanner, dest, reader)
return t.parse()
}
// RunesValueReader is a function that takes the given value (a slice of runes)
// and returns the parsed value
type RunesValueReader func([]rune) (interface{}, error)
// parser is a simple parser that takes a strvals line and parses it into a
// map representation.
//
// where sc is the source of the original data being parsed
// where data is the final parsed data from the parses with correct types
type parser struct {
sc *bytes.Buffer
data map[string]interface{}
reader RunesValueReader
}
func newParser(sc *bytes.Buffer, data map[string]interface{}, stringBool bool) *parser {
stringConverter := func(rs []rune) (interface{}, error) {
return typedVal(rs, stringBool), nil
}
return &parser{sc: sc, data: data, reader: stringConverter}
}
func newFileParser(sc *bytes.Buffer, data map[string]interface{}, reader RunesValueReader) *parser {
return &parser{sc: sc, data: data, reader: reader}
}
func (t *parser) parse() error {
for {
err := t.key(t.data)
if err == nil {
continue
}
if err == io.EOF {
return nil
}
return err
}
}
func runeSet(r []rune) map[rune]bool {
s := make(map[rune]bool, len(r))
for _, rr := range r {
s[rr] = true
}
return s
}
func (t *parser) key(data map[string]interface{}) (reterr error) {
defer func() {
if r := recover(); r != nil {
reterr = fmt.Errorf("unable to parse key: %s", r)
}
}()
stop := runeSet([]rune{'=', '[', ',', '.'})
for {
switch k, last, err := runesUntil(t.sc, stop); {
case err != nil:
if len(k) == 0 {
return err
}
return errors.Errorf("key %q has no value", string(k))
//set(data, string(k), "")
//return err
case last == '[':
// We are in a list index context, so we need to set an index.
i, err := t.keyIndex()
if err != nil {
return errors.Wrap(err, "error parsing index")
}
kk := string(k)
// Find or create target list
list := []interface{}{}
if _, ok := data[kk]; ok {
list = data[kk].([]interface{})
}
// Now we need to get the value after the ].
list, err = t.listItem(list, i)
set(data, kk, list)
return err
case last == '=':
//End of key. Consume =, Get value.
// FIXME: Get value list first
vl, e := t.valList()
switch e {
case nil:
set(data, string(k), vl)
return nil
case io.EOF:
set(data, string(k), "")
return e
case ErrNotList:
rs, e := t.val()
if e != nil && e != io.EOF {
return e
}
v, e := t.reader(rs)
set(data, string(k), v)
return e
default:
return e
}
case last == ',':
// No value given. Set the value to empty string. Return error.
set(data, string(k), "")
return errors.Errorf("key %q has no value (cannot end with ,)", string(k))
case last == '.':
// First, create or find the target map.
inner := map[string]interface{}{}
if _, ok := data[string(k)]; ok {
inner = data[string(k)].(map[string]interface{})
}
// Recurse
e := t.key(inner)
if len(inner) == 0 {
return errors.Errorf("key map %q has no value", string(k))
}
set(data, string(k), inner)
return e
}
}
}
func set(data map[string]interface{}, key string, val interface{}) {
// If key is empty, don't set it.
if len(key) == 0 {
return
}
data[key] = val
}
func setIndex(list []interface{}, index int, val interface{}) (l2 []interface{}, err error) {
// There are possible index values that are out of range on a target system
// causing a panic. This will catch the panic and return an error instead.
// The value of the index that causes a panic varies from system to system.
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("error processing index %d: %s", index, r)
}
}()
if index < 0 {
return list, fmt.Errorf("negative %d index not allowed", index)
}
if len(list) <= index {
newlist := make([]interface{}, index+1)
copy(newlist, list)
list = newlist
}
list[index] = val
return list, nil
}
func (t *parser) keyIndex() (int, error) {
// First, get the key.
stop := runeSet([]rune{']'})
v, _, err := runesUntil(t.sc, stop)
if err != nil {
return 0, err
}
// v should be the index
return strconv.Atoi(string(v))
}
func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) {
if i < 0 {
return list, fmt.Errorf("negative %d index not allowed", i)
}
stop := runeSet([]rune{'[', '.', '='})
switch k, last, err := runesUntil(t.sc, stop); {
case len(k) > 0:
return list, errors.Errorf("unexpected data at end of array index: %q", k)
case err != nil:
return list, err
case last == '=':
vl, e := t.valList()
switch e {
case nil:
return setIndex(list, i, vl)
case io.EOF:
return setIndex(list, i, "")
case ErrNotList:
rs, e := t.val()
if e != nil && e != io.EOF {
return list, e
}
v, e := t.reader(rs)
if e != nil {
return list, e
}
return setIndex(list, i, v)
default:
return list, e
}
case last == '[':
// now we have a nested list. Read the index and handle.
nextI, err := t.keyIndex()
if err != nil {
return list, errors.Wrap(err, "error parsing index")
}
var crtList []interface{}
if len(list) > i {
// If nested list already exists, take the value of list to next cycle.
existed := list[i]
if existed != nil {
crtList = list[i].([]interface{})
}
}
// Now we need to get the value after the ].
list2, err := t.listItem(crtList, nextI)
if err != nil {
return list, err
}
return setIndex(list, i, list2)
case last == '.':
// We have a nested object. Send to t.key
inner := map[string]interface{}{}
if len(list) > i {
var ok bool
inner, ok = list[i].(map[string]interface{})
if !ok {
// We have indices out of order. Initialize empty value.
list[i] = map[string]interface{}{}
inner = list[i].(map[string]interface{})
}
}
// Recurse
e := t.key(inner)
if e != nil {
return list, e
}
return setIndex(list, i, inner)
default:
return nil, errors.Errorf("parse error: unexpected token %v", last)
}
}
func (t *parser) val() ([]rune, error) {
stop := runeSet([]rune{','})
v, _, err := runesUntil(t.sc, stop)
return v, err
}
func (t *parser) valList() ([]interface{}, error) {
r, _, e := t.sc.ReadRune()
if e != nil {
return []interface{}{}, e
}
if r != '{' {
t.sc.UnreadRune()
return []interface{}{}, ErrNotList
}
list := []interface{}{}
stop := runeSet([]rune{',', '}'})
for {
switch rs, last, err := runesUntil(t.sc, stop); {
case err != nil:
if err == io.EOF {
err = errors.New("list must terminate with '}'")
}
return list, err
case last == '}':
// If this is followed by ',', consume it.
if r, _, e := t.sc.ReadRune(); e == nil && r != ',' {
t.sc.UnreadRune()
}
v, e := t.reader(rs)
list = append(list, v)
return list, e
case last == ',':
v, e := t.reader(rs)
if e != nil {
return list, e
}
list = append(list, v)
}
}
}
func runesUntil(in io.RuneReader, stop map[rune]bool) ([]rune, rune, error) {
v := []rune{}
for {
switch r, _, e := in.ReadRune(); {
case e != nil:
return v, r, e
case inMap(r, stop):
return v, r, nil
case r == '\\':
next, _, e := in.ReadRune()
if e != nil {
return v, next, e
}
v = append(v, next)
default:
v = append(v, r)
}
}
}
func inMap(k rune, m map[rune]bool) bool {
_, ok := m[k]
return ok
}
func typedVal(v []rune, st bool) interface{} {
val := string(v)
if st {
return val
}
if strings.EqualFold(val, "true") {
return true
}
if strings.EqualFold(val, "false") {
return false
}
if strings.EqualFold(val, "null") {
return nil
}
if strings.EqualFold(val, "0") {
return int64(0)
}
// If this value does not start with zero, try parsing it to an int
if len(val) != 0 && val[0] != '0' {
if iv, err := strconv.ParseInt(val, 10, 64); err == nil {
return iv
}
}
return val
}