Delete pkg directory

Signed-off-by: HARPHUNA <109001160+HARPHUNA@users.noreply.github.com>
pull/11491/head
HARPHUNA 3 years ago committed by GitHub
parent b35ff17cd2
commit 7ff00a3099
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,421 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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/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/registry"
"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 (cfg *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 := cfg.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 && cfg.RESTClientGetter != nil {
restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil {
return hs, b, "", err
}
files, err2 = engine.RenderWithClient(ch, values, restConfig)
} 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 (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
if cfg.Capabilities != nil {
return cfg.Capabilities, nil
}
dc, err := cfg.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) {
cfg.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err)
cfg.Log("WARNING: To fix this, kubectl delete apiservice <service-name>")
} else {
return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
}
}
cfg.Capabilities = &chartutil.Capabilities{
APIVersions: apiVersions,
KubeVersion: chartutil.KubeVersion{
Version: kubeVersion.GitVersion,
Major: kubeVersion.Major,
Minor: kubeVersion.Minor,
},
HelmVersion: chartutil.DefaultCapabilities.HelmVersion,
}
return cfg.Capabilities, nil
}
// KubernetesClientSet creates a new kubernetes ClientSet based on the configuration
func (cfg *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
conf, err := cfg.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 (cfg *Configuration) Now() time.Time {
return Timestamper()
}
func (cfg *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 cfg.Releases.Last(name)
}
return cfg.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 (cfg *Configuration) recordRelease(r *release.Release) {
if err := cfg.Releases.Update(r); err != nil {
cfg.Log("warning: Failed to update release %s: %s", r.Name, err)
}
}
// Init initializes the action configuration
func (cfg *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 cfg.Releases != nil {
if mem, ok := cfg.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)
}
cfg.RESTClientGetter = getter
cfg.KubeClient = kc
cfg.Releases = store
cfg.Log = log
return nil
}

@ -1,283 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"flag"
"io/ioutil"
"testing"
fakeclientset "k8s.io/client-go/kubernetes/fake"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
"helm.sh/helm/v3/pkg/time"
)
var verbose = flag.Bool("test.log", false, "enable test logging")
func actionConfigFixture(t *testing.T) *Configuration {
t.Helper()
registryClient, err := registry.NewClient()
if err != nil {
t.Fatal(err)
}
return &Configuration{
Releases: storage.Init(driver.NewMemory()),
KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: ioutil.Discard}},
Capabilities: chartutil.DefaultCapabilities,
RegistryClient: registryClient,
Log: func(format string, v ...interface{}) {
t.Helper()
if *verbose {
t.Logf(format, v...)
}
},
}
}
var manifestWithHook = `kind: ConfigMap
metadata:
name: test-cm
annotations:
"helm.sh/hook": post-install,pre-delete,post-upgrade
data:
name: value`
var manifestWithTestHook = `kind: Pod
metadata:
name: finding-nemo,
annotations:
"helm.sh/hook": test
spec:
containers:
- name: nemo-test
image: fake-image
cmd: fake-command
`
var rbacManifests = `apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: schedule-agents
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/log"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: schedule-agents
namespace: {{ default .Release.Namespace}}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: schedule-agents
subjects:
- kind: ServiceAccount
name: schedule-agents
namespace: {{ .Release.Namespace }}
`
type chartOptions struct {
*chart.Chart
}
type chartOption func(*chartOptions)
func buildChart(opts ...chartOption) *chart.Chart {
c := &chartOptions{
Chart: &chart.Chart{
// TODO: This should be more complete.
Metadata: &chart.Metadata{
APIVersion: "v1",
Name: "hello",
Version: "0.1.0",
},
// This adds a basic template and hooks.
Templates: []*chart.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithHook)},
},
},
}
for _, opt := range opts {
opt(c)
}
return c.Chart
}
func withName(name string) chartOption {
return func(opts *chartOptions) {
opts.Metadata.Name = name
}
}
func withSampleValues() chartOption {
values := map[string]interface{}{
"someKey": "someValue",
"nestedKey": map[string]interface{}{
"simpleKey": "simpleValue",
"anotherNestedKey": map[string]interface{}{
"yetAnotherNestedKey": map[string]interface{}{
"youReadyForAnotherNestedKey": "No",
},
},
},
}
return func(opts *chartOptions) {
opts.Values = values
}
}
func withValues(values map[string]interface{}) chartOption {
return func(opts *chartOptions) {
opts.Values = values
}
}
func withNotes(notes string) chartOption {
return func(opts *chartOptions) {
opts.Templates = append(opts.Templates, &chart.File{
Name: "templates/NOTES.txt",
Data: []byte(notes),
})
}
}
func withDependency(dependencyOpts ...chartOption) chartOption {
return func(opts *chartOptions) {
opts.AddDependency(buildChart(dependencyOpts...))
}
}
func withMetadataDependency(dependency chart.Dependency) chartOption {
return func(opts *chartOptions) {
opts.Metadata.Dependencies = append(opts.Metadata.Dependencies, &dependency)
}
}
func withSampleTemplates() chartOption {
return func(opts *chartOptions) {
sampleTemplates := []*chart.File{
// This adds basic templates and partials.
{Name: "templates/goodbye", Data: []byte("goodbye: world")},
{Name: "templates/empty", Data: []byte("")},
{Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
}
opts.Templates = append(opts.Templates, sampleTemplates...)
}
}
func withSampleIncludingIncorrectTemplates() chartOption {
return func(opts *chartOptions) {
sampleTemplates := []*chart.File{
// This adds basic templates and partials.
{Name: "templates/goodbye", Data: []byte("goodbye: world")},
{Name: "templates/empty", Data: []byte("")},
{Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")},
{Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)},
{Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)},
}
opts.Templates = append(opts.Templates, sampleTemplates...)
}
}
func withMultipleManifestTemplate() chartOption {
return func(opts *chartOptions) {
sampleTemplates := []*chart.File{
{Name: "templates/rbac", Data: []byte(rbacManifests)},
}
opts.Templates = append(opts.Templates, sampleTemplates...)
}
}
func withKube(version string) chartOption {
return func(opts *chartOptions) {
opts.Metadata.KubeVersion = version
}
}
// releaseStub creates a release stub, complete with the chartStub as its chart.
func releaseStub() *release.Release {
return namedReleaseStub("angry-panda", release.StatusDeployed)
}
func namedReleaseStub(name string, status release.Status) *release.Release {
now := time.Now()
return &release.Release{
Name: name,
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: status,
Description: "Named Release Stub",
},
Chart: buildChart(withSampleTemplates()),
Config: map[string]interface{}{"name": "value"},
Version: 1,
Hooks: []*release.Hook{
{
Name: "test-cm",
Kind: "ConfigMap",
Path: "test-cm",
Manifest: manifestWithHook,
Events: []release.HookEvent{
release.HookPostInstall,
release.HookPreDelete,
},
},
{
Name: "finding-nemo",
Kind: "Pod",
Path: "finding-nemo",
Manifest: manifestWithTestHook,
Events: []release.HookEvent{
release.HookTest,
},
},
},
}
}
func TestGetVersionSet(t *testing.T) {
client := fakeclientset.NewSimpleClientset()
vs, err := GetVersionSet(client.Discovery())
if err != nil {
t.Error(err)
}
if !vs.Has("v1") {
t.Errorf("Expected supported versions to at least include v1.")
}
if vs.Has("nosuchversion/v1") {
t.Error("Non-existent version is reported found.")
}
}

@ -1,230 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"io"
"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
ColumnWidth uint
}
// NewDependency creates a new Dependency object with the given configuration.
func NewDependency() *Dependency {
return &Dependency{
ColumnWidth: 80,
}
}
// 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 = d.ColumnWidth
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 Chart.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)
}
}
}

@ -1,152 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v3/internal/test"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
)
func TestList(t *testing.T) {
for _, tcase := range []struct {
chart string
golden string
}{
{
chart: "testdata/charts/chart-with-compressed-dependencies",
golden: "output/list-compressed-deps.txt",
},
{
chart: "testdata/charts/chart-with-compressed-dependencies-2.1.8.tgz",
golden: "output/list-compressed-deps-tgz.txt",
},
{
chart: "testdata/charts/chart-with-uncompressed-dependencies",
golden: "output/list-uncompressed-deps.txt",
},
{
chart: "testdata/charts/chart-with-uncompressed-dependencies-2.1.8.tgz",
golden: "output/list-uncompressed-deps-tgz.txt",
},
{
chart: "testdata/charts/chart-missing-deps",
golden: "output/list-missing-deps.txt",
},
} {
buf := bytes.Buffer{}
if err := NewDependency().List(tcase.chart, &buf); err != nil {
t.Fatal(err)
}
test.AssertGoldenString(t, buf.String(), tcase.golden)
}
}
// TestDependencyStatus_Dashes is a regression test to make sure that dashes in
// chart names do not cause resolution problems.
func TestDependencyStatus_Dashes(t *testing.T) {
// Make a temp dir
dir := t.TempDir()
chartpath := filepath.Join(dir, "charts")
if err := os.MkdirAll(chartpath, 0700); err != nil {
t.Fatal(err)
}
// Add some fake charts
first := buildChart(withName("first-chart"))
_, err := chartutil.Save(first, chartpath)
if err != nil {
t.Fatal(err)
}
second := buildChart(withName("first-chart-second-chart"))
_, err = chartutil.Save(second, chartpath)
if err != nil {
t.Fatal(err)
}
dep := &chart.Dependency{
Name: "first-chart",
Version: "0.1.0",
}
// Now try to get the deps
stat := NewDependency().dependencyStatus(dir, dep, first)
if stat != "ok" {
t.Errorf("Unexpected status: %q", stat)
}
}
func TestStatArchiveForStatus(t *testing.T) {
// Make a temp dir
dir := t.TempDir()
chartpath := filepath.Join(dir, "charts")
if err := os.MkdirAll(chartpath, 0700); err != nil {
t.Fatal(err)
}
// unsaved chart
lilith := buildChart(withName("lilith"))
// dep referring to chart
dep := &chart.Dependency{
Name: "lilith",
Version: "1.2.3",
}
is := assert.New(t)
lilithpath := filepath.Join(chartpath, "lilith-1.2.3.tgz")
is.Empty(statArchiveForStatus(lilithpath, dep))
// save the chart (version 0.1.0, because that is the default)
where, err := chartutil.Save(lilith, chartpath)
is.NoError(err)
// Should get "wrong version" because we asked for 1.2.3 and got 0.1.0
is.Equal("wrong version", statArchiveForStatus(where, dep))
// Break version on dep
dep = &chart.Dependency{
Name: "lilith",
Version: "1.2.3.4.5",
}
is.Equal("invalid version", statArchiveForStatus(where, dep))
// Break the name
dep = &chart.Dependency{
Name: "lilith2",
Version: "1.2.3",
}
is.Equal("misnamed", statArchiveForStatus(where, dep))
// Now create the right version
dep = &chart.Dependency{
Name: "lilith",
Version: "0.1.0",
}
is.Equal("ok", statArchiveForStatus(where, dep))
}

@ -1,22 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package action 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

@ -1,47 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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)
}

@ -1,60 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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
}

@ -1,58 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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)
}

@ -1,151 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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
}

@ -1,767 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"sync"
"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/registry"
"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"
)
// 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
// Lock to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex
}
// 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
// registryClient provides a registry client but is not added with
// options from a flag
registryClient *registry.Client
}
// NewInstall creates a new Install object with the given configuration.
func NewInstall(cfg *Configuration) *Install {
in := &Install{
cfg: cfg,
}
in.ChartPathOptions.registryClient = cfg.RegistryClient
return in
}
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) {
ctx := context.Background()
return i.RunWithContext(ctx, chrt, vals)
}
// Run executes the installation with Context
func (i *Install) RunWithContext(ctx context.Context, 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
}
if err := chartutil.ProcessDependencies(chrt, vals); 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")
}
// 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
}
rChan := make(chan resultMessage)
doneChan := make(chan struct{})
defer close(doneChan)
go i.performInstall(rChan, rel, toBeAdopted, resources)
go i.handleContext(ctx, rChan, doneChan, rel)
result := <-rChan
//start preformInstall go routine
return result.r, result.e
}
func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) {
// pre-install hooks
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
i.reportToRun(c, rel, fmt.Errorf("failed pre-install: %s", err))
return
}
}
// 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 {
i.reportToRun(c, rel, err)
return
}
} else if len(resources) > 0 {
if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil {
i.reportToRun(c, rel, err)
return
}
}
if i.Wait {
if i.WaitForJobs {
if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil {
i.reportToRun(c, rel, err)
return
}
} else {
if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil {
i.reportToRun(c, rel, err)
return
}
}
}
if !i.DisableHooks {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
i.reportToRun(c, rel, fmt.Errorf("failed post-install: %s", err))
return
}
}
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)
}
i.reportToRun(c, rel, nil)
}
func (i *Install) handleContext(ctx context.Context, c chan<- resultMessage, done chan struct{}, rel *release.Release) {
select {
case <-ctx.Done():
err := ctx.Err()
i.reportToRun(c, rel, err)
case <-done:
return
}
}
func (i *Install) reportToRun(c chan<- resultMessage, rel *release.Release, err error) {
i.Lock.Lock()
if err != nil {
rel, err = i.failRelease(rel, err)
}
c <- resultMessage{r: rel, e: err}
i.Lock.Unlock()
}
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 err := chartutil.ValidateReleaseName(start); err != nil {
return errors.Wrapf(err, "release name %q", start)
}
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) {
// If there is no registry client and the name is in an OCI registry return
// an error and a lookup will not occur.
if registry.IsOCI(name) && c.registryClient == nil {
return "", fmt.Errorf("unable to lookup chart %q, missing registry client", name)
}
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,
RegistryClient: c.registryClient,
}
if registry.IsOCI(name) {
dl.Options = append(dl.Options, getter.WithRegistryClient(c.registryClient))
}
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 {
return "", err
}
lname, err := filepath.Abs(filename)
if err != nil {
return filename, err
}
return lname, nil
}

@ -1,719 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v3/internal/test"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
helmtime "helm.sh/helm/v3/pkg/time"
)
type nameTemplateTestCase struct {
tpl string
expected string
expectedErrorStr string
}
func installAction(t *testing.T) *Install {
config := actionConfigFixture(t)
instAction := NewInstall(config)
instAction.Namespace = "spaced"
instAction.ReleaseName = "test-install-release"
return instAction
}
func TestInstallRelease(t *testing.T) {
is := assert.New(t)
req := require.New(t)
instAction := installAction(t)
vals := map[string]interface{}{}
ctx, done := context.WithCancel(context.Background())
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Equal(res.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Install complete")
// Detecting previous bug where context termination after successful release
// caused release to fail.
done()
time.Sleep(time.Millisecond * 100)
lastRelease, err := instAction.cfg.Releases.Last(rel.Name)
req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
}
func TestInstallReleaseWithValues(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
userVals := map[string]interface{}{
"nestedKey": map[string]interface{}{
"simpleKey": "simpleValue",
},
}
expectedUserValues := map[string]interface{}{
"nestedKey": map[string]interface{}{
"simpleKey": "simpleValue",
},
}
res, err := instAction.Run(buildChart(withSampleValues()), userVals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Equal(res.Name, "test-install-release", "Expected release name.")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Equal("Install complete", rel.Info.Description)
is.Equal(expectedUserValues, rel.Config)
}
func TestInstallReleaseClientOnly(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ClientOnly = true
instAction.Run(buildChart(), nil) // disregard output
is.Equal(instAction.cfg.Capabilities, chartutil.DefaultCapabilities)
is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: ioutil.Discard})
}
func TestInstallRelease_NoName(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = ""
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(), vals)
if err == nil {
t.Fatal("expected failure when no name is specified")
}
assert.Contains(t, err.Error(), "no name provided")
}
func TestInstallRelease_WithNotes(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("note here")), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Equal(res.Name, "with-notes")
is.Equal(res.Namespace, "spaced")
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
is.Len(rel.Hooks, 1)
is.Equal(rel.Hooks[0].Manifest, manifestWithHook)
is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall)
is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete")
is.NotEqual(len(res.Manifest), 0)
is.NotEqual(len(rel.Manifest), 0)
is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Equal(rel.Info.Description, "Install complete")
is.Equal(rel.Info.Notes, "note here")
}
func TestInstallRelease_WithNotesRendered(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.NoError(err)
expectedNotes := fmt.Sprintf("got-%s", res.Name)
is.Equal(expectedNotes, rel.Info.Notes)
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) {
// Regression: Make sure that the child's notes don't override the parent's
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.Equal("with-notes", rel.Name)
is.NoError(err)
is.Equal("parent", rel.Info.Notes)
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) {
// Regression: Make sure that the child's notes don't override the parent's
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "with-notes"
instAction.SubNotes = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
rel, err := instAction.cfg.Releases.Get(res.Name, res.Version)
is.Equal("with-notes", rel.Name)
is.NoError(err)
// test run can return as either 'parent\nchild' or 'child\nparent'
if !strings.Contains(rel.Info.Notes, "parent") && !strings.Contains(rel.Info.Notes, "child") {
t.Fatalf("Expected 'parent\nchild' or 'child\nparent', got '%s'", rel.Info.Notes)
}
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallRelease_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleTemplates()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world")
is.Contains(res.Manifest, "hello: Earth")
is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}")
is.NotContains(res.Manifest, "empty")
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Len(res.Hooks, 1)
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run")
is.Equal(res.Info.Description, "Dry run complete")
}
// Regression test for #7955: Lookup must not connect to Kubernetes on a dry-run.
func TestInstallRelease_DryRun_Lookup(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
vals := map[string]interface{}{}
mockChart := buildChart(withSampleTemplates())
mockChart.Templates = append(mockChart.Templates, &chart.File{
Name: "templates/lookup",
Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`),
})
res, err := instAction.Run(mockChart, vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.Contains(res.Manifest, "goodbye: map[]")
}
func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals)
expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh"
if err == nil {
t.Fatalf("Install should fail containing error: %s", expectedErr)
}
if err != nil {
is.Contains(err.Error(), expectedErr)
}
}
func TestInstallRelease_NoHooks(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DisableHooks = true
instAction.ReleaseName = "no-hooks"
instAction.cfg.Releases.Create(releaseStub())
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "hooks should not run with no-hooks")
}
func TestInstallRelease_FailedHooks(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "failed-hooks"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WatchUntilReadyError = fmt.Errorf("Failed watch")
instAction.cfg.KubeClient = failer
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
is.Error(err)
is.Contains(res.Info.Description, "failed post-install")
is.Equal(release.StatusFailed, res.Info.Status)
}
func TestInstallRelease_ReplaceRelease(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.Replace = true
rel := releaseStub()
rel.Info.Status = release.StatusUninstalled
instAction.cfg.Releases.Create(rel)
instAction.ReleaseName = rel.Name
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
is.NoError(err)
// This should have been auto-incremented
is.Equal(2, res.Version)
is.Equal(res.Name, rel.Name)
getres, err := instAction.cfg.Releases.Get(rel.Name, res.Version)
is.NoError(err)
is.Equal(getres.Info.Status, release.StatusDeployed)
}
func TestInstallRelease_KubeVersion(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withKube(">=0.0.0")), vals)
is.NoError(err)
// This should fail for a few hundred years
instAction.ReleaseName = "should-fail"
vals = map[string]interface{}{}
_, err = instAction.Run(buildChart(withKube(">=99.0.0")), vals)
is.Error(err)
is.Contains(err.Error(), "chart requires kubeVersion")
}
func TestInstallRelease_Wait(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "come-fail-away"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
instAction.cfg.KubeClient = failer
instAction.Wait = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
is.Error(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestInstallRelease_Wait_Interrupted(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "interrupted-release"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
instAction.cfg.KubeClient = failer
instAction.Wait = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(time.Second, cancel)
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
is.Error(err)
is.Contains(res.Info.Description, "Release \"interrupted-release\" failed: context canceled")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestInstallRelease_WaitForJobs(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "come-fail-away"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
instAction.cfg.KubeClient = failer
instAction.Wait = true
instAction.WaitForJobs = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
is.Error(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestInstallRelease_Atomic(t *testing.T) {
is := assert.New(t)
t.Run("atomic uninstall succeeds", func(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = "come-fail-away"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
instAction.cfg.KubeClient = failer
instAction.Atomic = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)
is.Error(err)
is.Contains(err.Error(), "I timed out")
is.Contains(err.Error(), "atomic")
// Now make sure it isn't in storage any more
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Equal(err, driver.ErrReleaseNotFound)
})
t.Run("atomic uninstall fails", func(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = "come-fail-away-with-me"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
failer.DeleteError = fmt.Errorf("uninstall fail")
instAction.cfg.KubeClient = failer
instAction.Atomic = true
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(), vals)
is.Error(err)
is.Contains(err.Error(), "I timed out")
is.Contains(err.Error(), "uninstall fail")
is.Contains(err.Error(), "an error occurred while uninstalling the release")
})
}
func TestInstallRelease_Atomic_Interrupted(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "interrupted-release"
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
instAction.cfg.KubeClient = failer
instAction.Atomic = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(time.Second, cancel)
res, err := instAction.RunWithContext(ctx, buildChart(), vals)
is.Error(err)
is.Contains(err.Error(), "context canceled")
is.Contains(err.Error(), "atomic")
is.Contains(err.Error(), "uninstalled")
// Now make sure it isn't in storage any more
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Equal(err, driver.ErrReleaseNotFound)
}
func TestNameTemplate(t *testing.T) {
testCases := []nameTemplateTestCase{
// Just a straight up nop please
{
tpl: "foobar",
expected: "foobar",
expectedErrorStr: "",
},
// Random numbers at the end for fun & profit
{
tpl: "foobar-{{randNumeric 6}}",
expected: "foobar-[0-9]{6}$",
expectedErrorStr: "",
},
// Random numbers in the middle for fun & profit
{
tpl: "foobar-{{randNumeric 4}}-baz",
expected: "foobar-[0-9]{4}-baz$",
expectedErrorStr: "",
},
// No such function
{
tpl: "foobar-{{randInteger}}",
expected: "",
expectedErrorStr: "function \"randInteger\" not defined",
},
// Invalid template
{
tpl: "foobar-{{",
expected: "",
expectedErrorStr: "template: name-template:1: unclosed action",
},
}
for _, tc := range testCases {
n, err := TemplateName(tc.tpl)
if err != nil {
if tc.expectedErrorStr == "" {
t.Errorf("Was not expecting error, but got: %v", err)
continue
}
re, compErr := regexp.Compile(tc.expectedErrorStr)
if compErr != nil {
t.Errorf("Expected error string failed to compile: %v", compErr)
continue
}
if !re.MatchString(err.Error()) {
t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err)
continue
}
}
if err == nil && tc.expectedErrorStr != "" {
t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr)
}
if tc.expected != "" {
re, err := regexp.Compile(tc.expected)
if err != nil {
t.Errorf("Expected string failed to compile: %v", err)
continue
}
if !re.MatchString(n) {
t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n)
}
}
}
}
func TestInstallReleaseOutputDir(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
vals := map[string]interface{}{}
dir := t.TempDir()
instAction.OutputDir = dir
_, err := instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
_, err = os.Stat(filepath.Join(dir, "hello/templates/goodbye"))
is.NoError(err)
_, err = os.Stat(filepath.Join(dir, "hello/templates/hello"))
is.NoError(err)
_, err = os.Stat(filepath.Join(dir, "hello/templates/with-partials"))
is.NoError(err)
_, err = os.Stat(filepath.Join(dir, "hello/templates/rbac"))
is.NoError(err)
test.AssertGoldenFile(t, filepath.Join(dir, "hello/templates/rbac"), "rbac.txt")
_, err = os.Stat(filepath.Join(dir, "hello/templates/empty"))
is.True(os.IsNotExist(err))
}
func TestInstallOutputDirWithReleaseName(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
vals := map[string]interface{}{}
dir := t.TempDir()
instAction.OutputDir = dir
instAction.UseReleaseName = true
instAction.ReleaseName = "madra"
newDir := filepath.Join(dir, instAction.ReleaseName)
_, err := instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
_, err = os.Stat(filepath.Join(newDir, "hello/templates/goodbye"))
is.NoError(err)
_, err = os.Stat(filepath.Join(newDir, "hello/templates/hello"))
is.NoError(err)
_, err = os.Stat(filepath.Join(newDir, "hello/templates/with-partials"))
is.NoError(err)
_, err = os.Stat(filepath.Join(newDir, "hello/templates/rbac"))
is.NoError(err)
test.AssertGoldenFile(t, filepath.Join(newDir, "hello/templates/rbac"), "rbac.txt")
_, err = os.Stat(filepath.Join(newDir, "hello/templates/empty"))
is.True(os.IsNotExist(err))
}
func TestNameAndChart(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
chartName := "./foo"
name, chrt, err := instAction.NameAndChart([]string{chartName})
if err != nil {
t.Fatal(err)
}
is.Equal(instAction.ReleaseName, name)
is.Equal(chartName, chrt)
instAction.GenerateName = true
_, _, err = instAction.NameAndChart([]string{"foo", chartName})
if err == nil {
t.Fatal("expected an error")
}
is.Equal("cannot set --generate-name and also specify a name", err.Error())
instAction.GenerateName = false
instAction.NameTemplate = "{{ . }}"
_, _, err = instAction.NameAndChart([]string{"foo", chartName})
if err == nil {
t.Fatal("expected an error")
}
is.Equal("cannot set --name-template and also specify a name", err.Error())
instAction.NameTemplate = ""
instAction.ReleaseName = ""
_, _, err = instAction.NameAndChart([]string{chartName})
if err == nil {
t.Fatal("expected an error")
}
is.Equal("must either provide a name or specify --generate-name", err.Error())
instAction.NameTemplate = ""
instAction.ReleaseName = ""
_, _, err = instAction.NameAndChart([]string{"foo", chartName, "bar"})
if err == nil {
t.Fatal("expected an error")
}
is.Equal("expected at most two arguments, unexpected arguments: bar", err.Error())
}
func TestNameAndChartGenerateName(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = ""
instAction.GenerateName = true
tests := []struct {
Name string
Chart string
ExpectedName string
}{
{
"local filepath",
"./chart",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"dot filepath",
".",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"empty filepath",
"",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"packaged chart",
"chart.tgz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"packaged chart with .tar.gz extension",
"chart.tar.gz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
{
"packaged chart with local extension",
"./chart.tgz",
fmt.Sprintf("chart-%d", helmtime.Now().Unix()),
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
name, chrt, err := instAction.NameAndChart([]string{tc.Chart})
if err != nil {
t.Fatal(err)
}
is.Equal(tc.ExpectedName, name)
is.Equal(tc.Chart, chrt)
})
}
}

@ -1,197 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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)
}

@ -1,129 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io/ioutil"
"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
Quiet 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
}
// HasWaringsOrErrors checks is LintResult has any warnings or errors
func HasWarningsOrErrors(result *LintResult) bool {
for _, msg := range result.Messages {
if msg.Severity > support.InfoSev {
return true
}
}
return false
}
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 := os.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
}

@ -1,160 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
)
var (
values = make(map[string]interface{})
namespace = "testNamespace"
strict = false
chart1MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-1"
chart2MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-2"
corruptedTgzChart = "testdata/charts/corrupted-compressed-chart.tgz"
chartWithNoTemplatesDir = "testdata/charts/chart-with-no-templates-dir"
)
func TestLintChart(t *testing.T) {
tests := []struct {
name string
chartPath string
err bool
}{
{
name: "decompressed-chart",
chartPath: "testdata/charts/decompressedchart/",
},
{
name: "archived-chart-path",
chartPath: "testdata/charts/compressedchart-0.1.0.tgz",
},
{
name: "archived-chart-path-with-hyphens",
chartPath: "testdata/charts/compressedchart-with-hyphens-0.1.0.tgz",
},
{
name: "archived-tar-gz-chart-path",
chartPath: "testdata/charts/compressedchart-0.1.0.tar.gz",
},
{
name: "invalid-archived-chart-path",
chartPath: "testdata/charts/invalidcompressedchart0.1.0.tgz",
err: true,
},
{
name: "chart-missing-manifest",
chartPath: "testdata/charts/chart-missing-manifest",
err: true,
},
{
name: "chart-with-schema",
chartPath: "testdata/charts/chart-with-schema",
},
{
name: "chart-with-schema-negative",
chartPath: "testdata/charts/chart-with-schema-negative",
},
{
name: "pre-release-chart",
chartPath: "testdata/charts/pre-release-chart-0.1.0-alpha.tgz",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, strict)
switch {
case err != nil && !tt.err:
t.Errorf("%s", err)
case err == nil && tt.err:
t.Errorf("Expected a chart parsing error")
}
})
}
}
func TestNonExistentChart(t *testing.T) {
t.Run("should error out for non existent tgz chart", func(t *testing.T) {
testCharts := []string{"non-existent-chart.tgz"}
expectedError := "unable to open tarball: open non-existent-chart.tgz: no such file or directory"
testLint := NewLint()
result := testLint.Run(testCharts, values)
if len(result.Errors) != 1 {
t.Error("expected one error, but got", len(result.Errors))
}
actual := result.Errors[0].Error()
if actual != expectedError {
t.Errorf("expected '%s', but got '%s'", expectedError, actual)
}
})
t.Run("should error out for corrupted tgz chart", func(t *testing.T) {
testCharts := []string{corruptedTgzChart}
expectedEOFError := "unable to extract tarball: EOF"
testLint := NewLint()
result := testLint.Run(testCharts, values)
if len(result.Errors) != 1 {
t.Error("expected one error, but got", len(result.Errors))
}
actual := result.Errors[0].Error()
if actual != expectedEOFError {
t.Errorf("expected '%s', but got '%s'", expectedEOFError, actual)
}
})
}
func TestLint_MultipleCharts(t *testing.T) {
testCharts := []string{chart2MultipleChartLint, chart1MultipleChartLint}
testLint := NewLint()
if result := testLint.Run(testCharts, values); len(result.Errors) > 0 {
t.Error(result.Errors)
}
}
func TestLint_EmptyResultErrors(t *testing.T) {
testCharts := []string{chart2MultipleChartLint}
testLint := NewLint()
if result := testLint.Run(testCharts, values); len(result.Errors) > 0 {
t.Error("Expected no error, got more")
}
}
func TestLint_ChartWithWarnings(t *testing.T) {
t.Run("should pass when not strict", func(t *testing.T) {
testCharts := []string{chartWithNoTemplatesDir}
testLint := NewLint()
testLint.Strict = false
if result := testLint.Run(testCharts, values); len(result.Errors) > 0 {
t.Error("Expected no error, got more")
}
})
t.Run("should fail with errors when strict", func(t *testing.T) {
testCharts := []string{chartWithNoTemplatesDir}
testLint := NewLint()
testLint.Strict = true
if result := testLint.Run(testCharts, values); len(result.Errors) != 1 {
t.Error("expected one error, but got", len(result.Errors))
}
})
}

@ -1,324 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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
NoHeaders 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
}

@ -1,368 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage"
)
func TestListStates(t *testing.T) {
for input, expect := range map[string]ListStates{
"deployed": ListDeployed,
"uninstalled": ListUninstalled,
"uninstalling": ListUninstalling,
"superseded": ListSuperseded,
"failed": ListFailed,
"pending-install": ListPendingInstall,
"pending-rollback": ListPendingRollback,
"pending-upgrade": ListPendingUpgrade,
"unknown": ListUnknown,
"totally made up key": ListUnknown,
} {
if expect != expect.FromName(input) {
t.Errorf("Expected %d for %s", expect, input)
}
// This is a cheap way to verify that ListAll actually allows everything but Unknown
if got := expect.FromName(input); got != ListUnknown && got&ListAll == 0 {
t.Errorf("Expected %s to match the ListAll filter", input)
}
}
filter := ListDeployed | ListPendingRollback
if status := filter.FromName("deployed"); filter&status == 0 {
t.Errorf("Expected %d to match mask %d", status, filter)
}
if status := filter.FromName("failed"); filter&status != 0 {
t.Errorf("Expected %d to fail to match mask %d", status, filter)
}
}
func TestList_Empty(t *testing.T) {
lister := NewList(actionConfigFixture(t))
list, err := lister.Run()
assert.NoError(t, err)
assert.Len(t, list, 0)
}
func newListFixture(t *testing.T) *List {
return NewList(actionConfigFixture(t))
}
func TestList_OneNamespace(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
}
func TestList_AllNamespaces(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
lister.AllNamespaces = true
lister.SetStateMask()
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
}
func TestList_Sort(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Sort = ByNameDesc // Other sorts are tested elsewhere
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
is.Equal("two", list[0].Name)
is.Equal("three", list[1].Name)
is.Equal("one", list[2].Name)
}
func TestList_Limit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
// Lex order means one, three, two
is.Equal("one", list[0].Name)
is.Equal("three", list[1].Name)
}
func TestList_BigLimit(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 20
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 3)
// Lex order means one, three, two
is.Equal("one", list[0].Name)
is.Equal("three", list[1].Name)
is.Equal("two", list[2].Name)
}
func TestList_LimitOffset(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 1
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 2)
// Lex order means one, three, two
is.Equal("three", list[0].Name)
is.Equal("two", list[1].Name)
}
func TestList_LimitOffsetOutOfBounds(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Limit = 2
lister.Offset = 3 // Last item is index 2
makeMeSomeReleases(lister.cfg.Releases, t)
list, err := lister.Run()
is.NoError(err)
is.Len(list, 0)
lister.Limit = 10
lister.Offset = 1
list, err = lister.Run()
is.NoError(err)
is.Len(list, 2)
}
func TestList_StateMask(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
makeMeSomeReleases(lister.cfg.Releases, t)
one, err := lister.cfg.Releases.Get("one", 1)
is.NoError(err)
one.SetStatus(release.StatusUninstalled, "uninstalled")
err = lister.cfg.Releases.Update(one)
is.NoError(err)
res, err := lister.Run()
is.NoError(err)
is.Len(res, 2)
is.Equal("three", res[0].Name)
is.Equal("two", res[1].Name)
lister.StateMask = ListUninstalled
res, err = lister.Run()
is.NoError(err)
is.Len(res, 1)
is.Equal("one", res[0].Name)
lister.StateMask |= ListDeployed
res, err = lister.Run()
is.NoError(err)
is.Len(res, 3)
}
func TestList_StateMaskWithStaleRevisions(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.StateMask = ListFailed
makeMeSomeReleasesWithStaleFailure(lister.cfg.Releases, t)
res, err := lister.Run()
is.NoError(err)
is.Len(res, 1)
// "dirty" release should _not_ be present as most recent
// release is deployed despite failed release in past
is.Equal("failed", res[0].Name)
}
func makeMeSomeReleasesWithStaleFailure(store *storage.Storage, t *testing.T) {
t.Helper()
one := namedReleaseStub("clean", release.StatusDeployed)
one.Namespace = "default"
one.Version = 1
two := namedReleaseStub("dirty", release.StatusDeployed)
two.Namespace = "default"
two.Version = 1
three := namedReleaseStub("dirty", release.StatusFailed)
three.Namespace = "default"
three.Version = 2
four := namedReleaseStub("dirty", release.StatusDeployed)
four.Namespace = "default"
four.Version = 3
five := namedReleaseStub("failed", release.StatusFailed)
five.Namespace = "default"
five.Version = 1
for _, rel := range []*release.Release{one, two, three, four, five} {
if err := store.Create(rel); err != nil {
t.Fatal(err)
}
}
all, err := store.ListReleases()
assert.NoError(t, err)
assert.Len(t, all, 5, "sanity test: five items added")
}
func TestList_Filter(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "th."
makeMeSomeReleases(lister.cfg.Releases, t)
res, err := lister.Run()
is.NoError(err)
is.Len(res, 1)
is.Equal("three", res[0].Name)
}
func TestList_FilterFailsCompile(t *testing.T) {
is := assert.New(t)
lister := newListFixture(t)
lister.Filter = "t[h.{{{"
makeMeSomeReleases(lister.cfg.Releases, t)
_, err := lister.Run()
is.Error(err)
}
func makeMeSomeReleases(store *storage.Storage, t *testing.T) {
t.Helper()
one := releaseStub()
one.Name = "one"
one.Namespace = "default"
one.Version = 1
two := releaseStub()
two.Name = "two"
two.Namespace = "default"
two.Version = 2
three := releaseStub()
three.Name = "three"
three.Namespace = "default"
three.Version = 3
for _, rel := range []*release.Release{one, two, three} {
if err := store.Create(rel); err != nil {
t.Fatal(err)
}
}
all, err := store.ListReleases()
assert.NoError(t, err)
assert.Len(t, all, 3, "sanity test: three items added")
}
func TestFilterLatestReleases(t *testing.T) {
t.Run("should filter old versions of the same release", func(t *testing.T) {
r1 := releaseStub()
r1.Name = "r"
r1.Version = 1
r2 := releaseStub()
r2.Name = "r"
r2.Version = 2
another := releaseStub()
another.Name = "another"
another.Version = 1
filteredList := filterLatestReleases([]*release.Release{r1, r2, another})
expectedFilteredList := []*release.Release{r2, another}
assert.ElementsMatch(t, expectedFilteredList, filteredList)
})
t.Run("should not filter out any version across namespaces", func(t *testing.T) {
r1 := releaseStub()
r1.Name = "r"
r1.Namespace = "default"
r1.Version = 1
r2 := releaseStub()
r2.Name = "r"
r2.Namespace = "testing"
r2.Version = 2
filteredList := filterLatestReleases([]*release.Release{r1, r2})
expectedFilteredList := []*release.Release{r1, r2}
assert.ElementsMatch(t, expectedFilteredList, filteredList)
})
}
func TestSelectorList(t *testing.T) {
r1 := releaseStub()
r1.Name = "r1"
r1.Version = 1
r1.Labels = map[string]string{"key": "value1"}
r2 := releaseStub()
r2.Name = "r2"
r2.Version = 1
r2.Labels = map[string]string{"key": "value2"}
r3 := releaseStub()
r3.Name = "r3"
r3.Version = 1
r3.Labels = map[string]string{}
lister := newListFixture(t)
for _, rel := range []*release.Release{r1, r2, r3} {
if err := lister.cfg.Releases.Create(rel); err != nil {
t.Fatal(err)
}
}
t.Run("should fail selector parsing", func(t *testing.T) {
is := assert.New(t)
lister.Selector = "a?=b"
_, err := lister.Run()
is.Error(err)
})
t.Run("should select one release with matching label", func(t *testing.T) {
lister.Selector = "key==value1"
res, _ := lister.Run()
expectedFilteredList := []*release.Release{r1}
assert.ElementsMatch(t, expectedFilteredList, res)
})
t.Run("should select two releases with non matching label", func(t *testing.T) {
lister.Selector = "key!=value1"
res, _ := lister.Run()
expectedFilteredList := []*release.Release{r2, r3}
assert.ElementsMatch(t, expectedFilteredList, res)
})
}

@ -1,182 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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)
}

@ -1,125 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/Masterminds/semver/v3"
"helm.sh/helm/v3/internal/test/ensure"
)
func TestPassphraseFileFetcher(t *testing.T) {
secret := "secret"
directory := ensure.TempFile(t, "passphrase-file", []byte(secret))
defer os.RemoveAll(directory)
fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil)
if err != nil {
t.Fatal("Unable to create passphraseFileFetcher", err)
}
passphrase, err := fetcher("key")
if err != nil {
t.Fatal("Unable to fetch passphrase")
}
if string(passphrase) != secret {
t.Errorf("Expected %s got %s", secret, string(passphrase))
}
}
func TestPassphraseFileFetcher_WithLineBreak(t *testing.T) {
secret := "secret"
directory := ensure.TempFile(t, "passphrase-file", []byte(secret+"\n\n."))
defer os.RemoveAll(directory)
fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil)
if err != nil {
t.Fatal("Unable to create passphraseFileFetcher", err)
}
passphrase, err := fetcher("key")
if err != nil {
t.Fatal("Unable to fetch passphrase")
}
if string(passphrase) != secret {
t.Errorf("Expected %s got %s", secret, string(passphrase))
}
}
func TestPassphraseFileFetcher_WithInvalidStdin(t *testing.T) {
directory := ensure.TempDir(t)
defer os.RemoveAll(directory)
stdin, err := ioutil.TempFile(directory, "non-existing")
if err != nil {
t.Fatal("Unable to create test file", err)
}
if _, err := passphraseFileFetcher("-", stdin); err == nil {
t.Error("Expected passphraseFileFetcher returning an error")
}
}
func TestValidateVersion(t *testing.T) {
type args struct {
ver string
}
tests := []struct {
name string
args args
wantErr error
}{
{
"normal semver version",
args{
ver: "1.1.3-23658",
},
nil,
},
{
"Pre version number starting with 0",
args{
ver: "1.1.3-023658",
},
semver.ErrSegmentStartsZero,
},
{
"Invalid version number",
args{
ver: "1.1.3.sd.023658",
},
semver.ErrInvalidSemVer,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateVersion(tt.args.ver); err != nil {
if err != tt.wantErr {
t.Errorf("Expected {%v}, got {%v}", tt.wantErr, err)
}
}
})
}
}

@ -1,166 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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/registry"
"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),
},
RegistryClient: p.cfg.RegistryClient,
RepositoryConfig: p.Settings.RepositoryConfig,
RepositoryCache: p.Settings.RepositoryCache,
}
if registry.IsOCI(chartRef) {
c.Options = append(c.Options,
getter.WithRegistryClient(p.cfg.RegistryClient))
}
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
}

@ -1,70 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"strings"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/pusher"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/uploader"
)
// Push is the action for uploading a chart.
//
// It provides the implementation of 'helm push'.
type Push struct {
Settings *cli.EnvSettings
cfg *Configuration
}
// PushOpt is a type of function that sets options for a push action.
type PushOpt func(*Push)
// WithPushConfig sets the cfg field on the push configuration object.
func WithPushConfig(cfg *Configuration) PushOpt {
return func(p *Push) {
p.cfg = cfg
}
}
// NewPushWithOpts creates a new push, with configuration options.
func NewPushWithOpts(opts ...PushOpt) *Push {
p := &Push{}
for _, fn := range opts {
fn(p)
}
return p
}
// Run executes 'helm push' against the given chart archive.
func (p *Push) Run(chartRef string, remote string) (string, error) {
var out strings.Builder
c := uploader.ChartUploader{
Out: &out,
Pushers: pusher.All(p.Settings),
Options: []pusher.Option{},
}
if registry.IsOCI(remote) {
c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient))
}
return out.String(), c.UploadTo(chartRef, remote)
}

@ -1,43 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
"helm.sh/helm/v3/pkg/registry"
)
// 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,
registry.LoginOptBasicAuth(username, password),
registry.LoginOptInsecure(insecure))
}

@ -1,38 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"io"
)
// 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)
}

@ -1,138 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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
}

@ -1,46 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package 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
}

@ -1,246 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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)
}
// It is safe to use "force" here because these are resources currently rendered by the chart.
err = target.Visit(setMetadataVisitor(targetRelease.Name, targetRelease.Namespace, true))
if err != nil {
return targetRelease, errors.Wrap(err, "unable to set metadata visitor from target release")
}
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
}

@ -1,156 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"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"
// ShowCRDs is the format which only shows the chart's CRDs
ShowCRDs ShowOutputFormat = "crds"
)
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.
// Deprecated: Use NewShowWithConfig
// TODO Helm 4: Fold NewShowWithConfig back into NewShow
func NewShow(output ShowOutputFormat) *Show {
return &Show{
OutputFormat: output,
}
}
// NewShowWithConfig creates a new Show object with the given configuration.
func NewShowWithConfig(output ShowOutputFormat, cfg *Configuration) *Show {
sh := &Show{
OutputFormat: output,
}
sh.ChartPathOptions.registryClient = cfg.RegistryClient
return sh
}
// 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 {
readme := findReadme(s.chart.Files)
if readme != nil {
if s.OutputFormat == ShowAll {
fmt.Fprintln(&out, "---")
}
fmt.Fprintf(&out, "%s\n", readme.Data)
}
}
if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll {
crds := s.chart.CRDObjects()
if len(crds) > 0 {
if s.OutputFormat == ShowAll && !bytes.HasPrefix(crds[0].File.Data, []byte("---")) {
fmt.Fprintln(&out, "---")
}
for _, crd := range crds {
fmt.Fprintf(&out, "%s\n", string(crd.File.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
}

@ -1,153 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
"helm.sh/helm/v3/pkg/chart"
)
func TestShow(t *testing.T) {
config := actionConfigFixture(t)
client := NewShowWithConfig(ShowAll, config)
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "README.md", Data: []byte("README\n")},
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
Raw: []*chart.File{
{Name: "values.yaml", Data: []byte("VALUES\n")},
},
Values: map[string]interface{}{},
}
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
expect := `name: alpine
---
VALUES
---
README
---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}
func TestShowNoValues(t *testing.T) {
client := NewShow(ShowAll)
client.chart = new(chart.Chart)
// Regression tests for missing values. See issue #1024.
client.OutputFormat = ShowValues
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
if len(output) != 0 {
t.Errorf("expected empty values buffer, got %s", output)
}
}
func TestShowValuesByJsonPathFormat(t *testing.T) {
client := NewShow(ShowValues)
client.JSONPathTemplate = "{$.nestedKey.simpleKey}"
client.chart = buildChart(withSampleValues())
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
expect := "simpleValue"
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}
func TestShowCRDs(t *testing.T) {
client := NewShow(ShowCRDs)
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
}
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
expect := `---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}
func TestShowNoReadme(t *testing.T) {
client := NewShow(ShowAll)
client.chart = &chart.Chart{
Metadata: &chart.Metadata{Name: "alpine"},
Files: []*chart.File{
{Name: "crds/ignoreme.txt", Data: []byte("error")},
{Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")},
{Name: "crds/bar.json", Data: []byte("---\nbar\n")},
},
}
output, err := client.Run("")
if err != nil {
t.Fatal(err)
}
expect := `name: alpine
---
foo
---
bar
`
if output != expect {
t.Errorf("Expected\n%q\nGot\n%q\n", expect, output)
}
}

@ -1,51 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"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)
}

@ -1,2 +0,0 @@
name: chart-with-missing-deps
version: 2.1.8

@ -1,6 +0,0 @@
dependencies:
- name: mariadb
repository: https://charts.helm.sh/stable/
version: 4.3.1
digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26
generated: 2018-08-02T22:07:51.905271776Z

@ -1,7 +0,0 @@
dependencies:
- name: mariadb
version: 4.x.x
repository: https://charts.helm.sh/stable/
condition: mariadb.enabled
tags:
- wordpress-database

@ -1,2 +0,0 @@
name: chart-with-compressed-dependencies
version: 2.1.8

@ -1,6 +0,0 @@
dependencies:
- name: mariadb
repository: https://charts.helm.sh/stable/
version: 4.3.1
digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26
generated: 2018-08-02T22:07:51.905271776Z

@ -1,7 +0,0 @@
dependencies:
- name: mariadb
version: 4.x.x
repository: https://charts.helm.sh/stable/
condition: mariadb.enabled
tags:
- wordpress-database

@ -1,5 +0,0 @@
apiVersion: v1
name: chart-with-no-templates-dir
description: an example chart
version: 199.44.12345-Alpha.1+cafe009
icon: http://riverrun.io

@ -1,7 +0,0 @@
apiVersion: v1
description: Empty testing chart
home: https://k8s.io/helm
name: empty
sources:
- https://github.com/kubernetes/helm
version: 0.1.0

@ -1,67 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"addresses": {
"description": "List of addresses",
"items": {
"properties": {
"city": {
"type": "string"
},
"number": {
"type": "number"
},
"street": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"age": {
"description": "Age",
"minimum": 0,
"type": "integer"
},
"employmentInfo": {
"properties": {
"salary": {
"minimum": 0,
"type": "number"
},
"title": {
"type": "string"
}
},
"required": [
"salary"
],
"type": "object"
},
"firstname": {
"description": "First name",
"type": "string"
},
"lastname": {
"type": "string"
},
"likesCoffee": {
"type": "boolean"
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"firstname",
"lastname",
"addresses",
"employmentInfo"
],
"title": "Values",
"type": "object"
}

@ -1,14 +0,0 @@
firstname: John
lastname: Doe
age: -5
likesCoffee: true
addresses:
- city: Springfield
street: Main
number: 12345
- city: New York
street: Broadway
number: 67890
phoneNumbers:
- "(888) 888-8888"
- "(555) 555-5555"

@ -1,7 +0,0 @@
apiVersion: v1
description: Empty testing chart
home: https://k8s.io/helm
name: empty
sources:
- https://github.com/kubernetes/helm
version: 0.1.0

@ -1,2 +0,0 @@
age: -5
employmentInfo: null

@ -1 +0,0 @@
# This file is intentionally blank

@ -1,67 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"addresses": {
"description": "List of addresses",
"items": {
"properties": {
"city": {
"type": "string"
},
"number": {
"type": "number"
},
"street": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"age": {
"description": "Age",
"minimum": 0,
"type": "integer"
},
"employmentInfo": {
"properties": {
"salary": {
"minimum": 0,
"type": "number"
},
"title": {
"type": "string"
}
},
"required": [
"salary"
],
"type": "object"
},
"firstname": {
"description": "First name",
"type": "string"
},
"lastname": {
"type": "string"
},
"likesCoffee": {
"type": "boolean"
},
"phoneNumbers": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"firstname",
"lastname",
"addresses",
"employmentInfo"
],
"title": "Values",
"type": "object"
}

@ -1,17 +0,0 @@
firstname: John
lastname: Doe
age: 25
likesCoffee: true
employmentInfo:
title: Software Developer
salary: 100000
addresses:
- city: Springfield
street: Main
number: 12345
- city: New York
street: Broadway
number: 67890
phoneNumbers:
- "(888) 888-8888"
- "(555) 555-5555"

@ -1,5 +0,0 @@
.git
# OWNERS file for Kubernetes
OWNERS
# example production yaml
values-production.yaml

@ -1,20 +0,0 @@
appVersion: 4.9.8
description: Web publishing platform for building blogs and websites.
engine: gotpl
home: http://www.wordpress.com/
icon: https://bitnami.com/assets/stacks/wordpress/img/wordpress-stack-220x234.png
keywords:
- wordpress
- cms
- blog
- http
- web
- application
- php
maintainers:
- email: containers@bitnami.com
name: bitnami-bot
name: chart-with-uncompressed-dependencies
sources:
- https://github.com/bitnami/bitnami-docker-wordpress
version: 2.1.8

@ -1,3 +0,0 @@
# WordPress
This is a testing mock, and is not operational.

@ -1,21 +0,0 @@
appVersion: 10.1.34
description: Fast, reliable, scalable, and easy to use open-source relational database
system. MariaDB Server is intended for mission-critical, heavy-load production systems
as well as for embedding into mass-deployed software. Highly available MariaDB cluster.
engine: gotpl
home: https://mariadb.org
icon: https://bitnami.com/assets/stacks/mariadb/img/mariadb-stack-220x234.png
keywords:
- mariadb
- mysql
- database
- sql
- prometheus
maintainers:
- email: containers@bitnami.com
name: bitnami-bot
name: mariadb
sources:
- https://github.com/bitnami/bitnami-docker-mariadb
- https://github.com/prometheus/mysqld_exporter
version: 4.3.1

@ -1,143 +0,0 @@
# MariaDB
[MariaDB](https://mariadb.org) is one of the most popular database servers in the world. Its made by the original developers of MySQL and guaranteed to stay open source. Notable users include Wikipedia, Facebook and Google.
MariaDB is developed as open source software and as a relational database it provides an SQL interface for accessing data. The latest versions of MariaDB also include GIS and JSON features.
## TL;DR
```bash
$ helm install stable/mariadb
```
## Introduction
This chart bootstraps a [MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) replication cluster deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
## Prerequisites
- Kubernetes 1.4+ with Beta APIs enabled
- PV provisioner support in the underlying infrastructure
## Installing the Chart
To install the chart with the release name `my-release`:
```bash
$ helm install --name my-release stable/mariadb
```
The command deploys MariaDB on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation.
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart
To uninstall/delete the `my-release` deployment:
```bash
$ helm delete my-release
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
## Configuration
The following table lists the configurable parameters of the MariaDB chart and their default values.
| Parameter | Description | Default |
|-------------------------------------------|-----------------------------------------------------|-------------------------------------------------------------------|
| `image.registry` | MariaDB image registry | `docker.io` |
| `image.repository` | MariaDB Image name | `bitnami/mariadb` |
| `image.tag` | MariaDB Image tag | `{VERSION}` |
| `image.pullPolicy` | MariaDB image pull policy | `Always` if `imageTag` is `latest`, else `IfNotPresent` |
| `image.pullSecrets` | Specify image pull secrets | `nil` (does not add image pull secrets to deployed pods) |
| `service.type` | Kubernetes service type | `ClusterIP` |
| `service.port` | MySQL service port | `3306` |
| `rootUser.password` | Password for the `root` user | _random 10 character alphanumeric string_ |
| `rootUser.forcePassword` | Force users to specify a password | `false` |
| `db.user` | Username of new user to create | `nil` |
| `db.password` | Password for the new user | _random 10 character alphanumeric string if `db.user` is defined_ |
| `db.name` | Name for new database to create | `my_database` |
| `replication.enabled` | MariaDB replication enabled | `true` |
| `replication.user` | MariaDB replication user | `replicator` |
| `replication.password` | MariaDB replication user password | _random 10 character alphanumeric string_ |
| `master.antiAffinity` | Master pod anti-affinity policy | `soft` |
| `master.persistence.enabled` | Enable persistence using a `PersistentVolumeClaim` | `true` |
| `master.persistence.annotations` | Persistent Volume Claim annotations | `{}` |
| `master.persistence.storageClass` | Persistent Volume Storage Class | `` |
| `master.persistence.accessModes` | Persistent Volume Access Modes | `[ReadWriteOnce]` |
| `master.persistence.size` | Persistent Volume Size | `8Gi` |
| `master.config` | Config file for the MariaDB Master server | `_default values in the values.yaml file_` |
| `master.resources` | CPU/Memory resource requests/limits for master node | `{}` |
| `master.livenessProbe.enabled` | Turn on and off liveness probe (master) | `true` |
| `master.livenessProbe.initialDelaySeconds`| Delay before liveness probe is initiated (master) | `120` |
| `master.livenessProbe.periodSeconds` | How often to perform the probe (master) | `10` |
| `master.livenessProbe.timeoutSeconds` | When the probe times out (master) | `1` |
| `master.livenessProbe.successThreshold` | Minimum consecutive successes for the probe (master)| `1` |
| `master.livenessProbe.failureThreshold` | Minimum consecutive failures for the probe (master) | `3` |
| `master.readinessProbe.enabled` | Turn on and off readiness probe (master) | `true` |
| `master.readinessProbe.initialDelaySeconds`| Delay before readiness probe is initiated (master) | `15` |
| `master.readinessProbe.periodSeconds` | How often to perform the probe (master) | `10` |
| `master.readinessProbe.timeoutSeconds` | When the probe times out (master) | `1` |
| `master.readinessProbe.successThreshold` | Minimum consecutive successes for the probe (master)| `1` |
| `master.readinessProbe.failureThreshold` | Minimum consecutive failures for the probe (master) | `3` |
| `slave.replicas` | Desired number of slave replicas | `1` |
| `slave.antiAffinity` | Slave pod anti-affinity policy | `soft` |
| `slave.persistence.enabled` | Enable persistence using a `PersistentVolumeClaim` | `true` |
| `slave.persistence.annotations` | Persistent Volume Claim annotations | `{}` |
| `slave.persistence.storageClass` | Persistent Volume Storage Class | `` |
| `slave.persistence.accessModes` | Persistent Volume Access Modes | `[ReadWriteOnce]` |
| `slave.persistence.size` | Persistent Volume Size | `8Gi` |
| `slave.config` | Config file for the MariaDB Slave replicas | `_default values in the values.yaml file_` |
| `slave.resources` | CPU/Memory resource requests/limits for slave node | `{}` |
| `slave.livenessProbe.enabled` | Turn on and off liveness probe (slave) | `true` |
| `slave.livenessProbe.initialDelaySeconds` | Delay before liveness probe is initiated (slave) | `120` |
| `slave.livenessProbe.periodSeconds` | How often to perform the probe (slave) | `10` |
| `slave.livenessProbe.timeoutSeconds` | When the probe times out (slave) | `1` |
| `slave.livenessProbe.successThreshold` | Minimum consecutive successes for the probe (slave) | `1` |
| `slave.livenessProbe.failureThreshold` | Minimum consecutive failures for the probe (slave) | `3` |
| `slave.readinessProbe.enabled` | Turn on and off readiness probe (slave) | `true` |
| `slave.readinessProbe.initialDelaySeconds`| Delay before readiness probe is initiated (slave) | `15` |
| `slave.readinessProbe.periodSeconds` | How often to perform the probe (slave) | `10` |
| `slave.readinessProbe.timeoutSeconds` | When the probe times out (slave) | `1` |
| `slave.readinessProbe.successThreshold` | Minimum consecutive successes for the probe (slave) | `1` |
| `slave.readinessProbe.failureThreshold` | Minimum consecutive failures for the probe (slave) | `3` |
| `metrics.enabled` | Start a side-car prometheus exporter | `false` |
| `metrics.image.registry` | Exporter image registry | `docker.io` |
`metrics.image.repository` | Exporter image name | `prom/mysqld-exporter` |
| `metrics.image.tag` | Exporter image tag | `v0.10.0` |
| `metrics.image.pullPolicy` | Exporter image pull policy | `IfNotPresent` |
| `metrics.resources` | Exporter resource requests/limit | `nil` |
The above parameters map to the env variables defined in [bitnami/mariadb](http://github.com/bitnami/bitnami-docker-mariadb). For more information please refer to the [bitnami/mariadb](http://github.com/bitnami/bitnami-docker-mariadb) image documentation.
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,
```bash
$ helm install --name my-release \
--set root.password=secretpassword,user.database=app_database \
stable/mariadb
```
The above command sets the MariaDB `root` account password to `secretpassword`. Additionally it creates a database named `my_database`.
Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example,
```bash
$ helm install --name my-release -f values.yaml stable/mariadb
```
> **Tip**: You can use the default [values.yaml](values.yaml)
## Initialize a fresh instance
The [Bitnami MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) image allows you to use your custom scripts to initialize a fresh instance. In order to execute the scripts, they must be located inside the chart folder `files/docker-entrypoint-initdb.d` so they can be consumed as a ConfigMap.
The allowed extensions are `.sh`, `.sql` and `.sql.gz`.
## Persistence
The [Bitnami MariaDB](https://github.com/bitnami/bitnami-docker-mariadb) image stores the MariaDB data and configurations at the `/bitnami/mariadb` path of the container.
The chart mounts a [Persistent Volume](kubernetes.io/docs/user-guide/persistent-volumes/) volume at this location. The volume is created using dynamic volume provisioning, by default. An existing PersistentVolumeClaim can be defined.

@ -1,3 +0,0 @@
You can copy here your custom .sh, .sql or .sql.gz file so they are executed during the first boot of the image.
More info in the [bitnami-docker-mariadb](https://github.com/bitnami/bitnami-docker-mariadb#initializing-a-new-instance) repository.

@ -1,35 +0,0 @@
Please be patient while the chart is being deployed
Tip:
Watch the deployment status using the command: kubectl get pods -w --namespace {{ .Release.Namespace }} -l release={{ .Release.Name }}
Services:
echo Master: {{ template "mariadb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}
{{- if .Values.replication.enabled }}
echo Slave: {{ template "slave.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:{{ .Values.service.port }}
{{- end }}
Administrator credentials:
Username: root
Password : $(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "mariadb.fullname" . }} -o jsonpath="{.data.mariadb-root-password}" | base64 --decode)
To connect to your database
1. Run a pod that you can use as a client:
kubectl run {{ template "mariadb.fullname" . }}-client --rm --tty -i --image {{ template "mariadb.image" . }} --namespace {{ .Release.Namespace }} --command -- bash
2. To connect to master service (read/write):
mysql -h {{ template "mariadb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local -uroot -p {{ .Values.db.name }}
{{- if .Values.replication.enabled }}
3. To connect to slave service (read-only):
mysql -h {{ template "slave.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local -uroot -p {{ .Values.db.name }}
{{- end }}

@ -1,53 +0,0 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "mariadb.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "mariadb.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "master.fullname" -}}
{{- if .Values.replication.enabled -}}
{{- printf "%s-%s" .Release.Name "mariadb-master" | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name "mariadb" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "slave.fullname" -}}
{{- printf "%s-%s" .Release.Name "mariadb-slave" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "mariadb.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Return the proper image name
*/}}
{{- define "mariadb.image" -}}
{{- $registryName := .Values.image.registry -}}
{{- $repositoryName := .Values.image.repository -}}
{{- $tag := .Values.image.tag | toString -}}
{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}}
{{- end -}}
{{/*
Return the proper image name
*/}}
{{- define "metrics.image" -}}
{{- $registryName := .Values.metrics.image.registry -}}
{{- $repositoryName := .Values.metrics.image.repository -}}
{{- $tag := .Values.metrics.image.tag | toString -}}
{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}}
{{- end -}}

@ -1,12 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "master.fullname" . }}-init-scripts
labels:
app: {{ template "mariadb.name" . }}
component: "master"
chart: {{ template "mariadb.chart" . }}
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
data:
{{ (.Files.Glob "files/docker-entrypoint-initdb.d/*").AsConfig | indent 2 }}

@ -1,15 +0,0 @@
{{- if .Values.master.config }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "master.fullname" . }}
labels:
app: {{ template "mariadb.name" . }}
component: "master"
chart: {{ template "mariadb.chart" . }}
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
data:
my.cnf: |-
{{ .Values.master.config | indent 4 }}
{{- end -}}

@ -1,187 +0,0 @@
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: {{ template "master.fullname" . }}
labels:
app: "{{ template "mariadb.name" . }}"
chart: {{ template "mariadb.chart" . }}
component: "master"
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
spec:
serviceName: "{{ template "master.fullname" . }}"
replicas: 1
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: "{{ template "mariadb.name" . }}"
component: "master"
release: "{{ .Release.Name }}"
chart: {{ template "mariadb.chart" . }}
spec:
securityContext:
runAsUser: 1001
fsGroup: 1001
{{- if eq .Values.master.antiAffinity "hard" }}
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: "kubernetes.io/hostname"
labelSelector:
matchLabels:
app: "{{ template "mariadb.name" . }}"
release: "{{ .Release.Name }}"
{{- else if eq .Values.master.antiAffinity "soft" }}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: "{{ template "mariadb.name" . }}"
release: "{{ .Release.Name }}"
{{- end }}
{{- if .Values.image.pullSecrets }}
imagePullSecrets:
{{- range .Values.image.pullSecrets }}
- name: {{ . }}
{{- end}}
{{- end }}
containers:
- name: "mariadb"
image: {{ template "mariadb.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy | quote }}
env:
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-root-password
{{- if .Values.db.user }}
- name: MARIADB_USER
value: "{{ .Values.db.user }}"
- name: MARIADB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-password
{{- end }}
- name: MARIADB_DATABASE
value: "{{ .Values.db.name }}"
{{- if .Values.replication.enabled }}
- name: MARIADB_REPLICATION_MODE
value: "master"
- name: MARIADB_REPLICATION_USER
value: "{{ .Values.replication.user }}"
- name: MARIADB_REPLICATION_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-replication-password
{{- end }}
ports:
- name: mysql
containerPort: 3306
{{- if .Values.master.livenessProbe.enabled }}
livenessProbe:
exec:
command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"]
initialDelaySeconds: {{ .Values.master.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.master.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.master.livenessProbe.timeoutSeconds }}
successThreshold: {{ .Values.master.livenessProbe.successThreshold }}
failureThreshold: {{ .Values.master.livenessProbe.failureThreshold }}
{{- end }}
{{- if .Values.master.readinessProbe.enabled }}
readinessProbe:
exec:
command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"]
initialDelaySeconds: {{ .Values.master.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.master.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.master.readinessProbe.timeoutSeconds }}
successThreshold: {{ .Values.master.readinessProbe.successThreshold }}
failureThreshold: {{ .Values.master.readinessProbe.failureThreshold }}
{{- end }}
resources:
{{ toYaml .Values.master.resources | indent 10 }}
volumeMounts:
- name: data
mountPath: /bitnami/mariadb
- name: custom-init-scripts
mountPath: /docker-entrypoint-initdb.d
{{- if .Values.master.config }}
- name: config
mountPath: /opt/bitnami/mariadb/conf/my.cnf
subPath: my.cnf
{{- end }}
{{- if .Values.metrics.enabled }}
- name: metrics
image: {{ template "metrics.image" . }}
imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }}
env:
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-root-password
command: [ 'sh', '-c', 'DATA_SOURCE_NAME="root:$MARIADB_ROOT_PASSWORD@(localhost:3306)/" /bin/mysqld_exporter' ]
ports:
- name: metrics
containerPort: 9104
livenessProbe:
httpGet:
path: /metrics
port: metrics
initialDelaySeconds: 15
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /metrics
port: metrics
initialDelaySeconds: 5
timeoutSeconds: 1
resources:
{{ toYaml .Values.metrics.resources | indent 10 }}
{{- end }}
volumes:
{{- if .Values.master.config }}
- name: config
configMap:
name: {{ template "master.fullname" . }}
{{- end }}
- name: custom-init-scripts
configMap:
name: {{ template "master.fullname" . }}-init-scripts
{{- if .Values.master.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
labels:
app: "{{ template "mariadb.name" . }}"
chart: {{ template "mariadb.chart" . }}
component: "master"
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
spec:
accessModes:
{{- range .Values.master.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.master.persistence.size | quote }}
{{- if .Values.master.persistence.storageClass }}
{{- if (eq "-" .Values.master.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.master.persistence.storageClass | quote }}
{{- end }}
{{- end }}
{{- else }}
- name: "data"
emptyDir: {}
{{- end }}

@ -1,29 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ template "mariadb.fullname" . }}
labels:
app: "{{ template "mariadb.name" . }}"
component: "master"
chart: {{ template "mariadb.chart" . }}
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
{{- if .Values.metrics.enabled }}
annotations:
{{ toYaml .Values.metrics.annotations | indent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- name: mysql
port: {{ .Values.service.port }}
targetPort: mysql
{{- if .Values.metrics.enabled }}
- name: metrics
port: 9104
targetPort: metrics
{{- end }}
selector:
app: "{{ template "mariadb.name" . }}"
component: "master"
release: "{{ .Release.Name }}"

@ -1,38 +0,0 @@
{{- if (not .Values.rootUser.existingSecret) -}}
apiVersion: v1
kind: Secret
metadata:
name: {{ template "mariadb.fullname" . }}
labels:
app: "{{ template "mariadb.name" . }}"
chart: {{ template "mariadb.chart" . }}
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
type: Opaque
data:
{{- if .Values.rootUser.password }}
mariadb-root-password: "{{ .Values.rootUser.password | b64enc }}"
{{- else if (not .Values.rootUser.forcePassword) }}
mariadb-root-password: "{{ randAlphaNum 10 | b64enc }}"
{{ else }}
mariadb-root-password: {{ required "A MariaDB Root Password is required!" .Values.rootUser.password }}
{{- end }}
{{- if .Values.db.user }}
{{- if .Values.db.password }}
mariadb-password: "{{ .Values.db.password | b64enc }}"
{{- else if (not .Values.db.forcePassword) }}
mariadb-password: "{{ randAlphaNum 10 | b64enc }}"
{{- else }}
mariadb-password: {{ required "A MariaDB Database Password is required!" .Values.db.password }}
{{- end }}
{{- end }}
{{- if .Values.replication.enabled }}
{{- if .Values.replication.password }}
mariadb-replication-password: "{{ .Values.replication.password | b64enc }}"
{{- else if (not .Values.replication.forcePassword) }}
mariadb-replication-password: "{{ randAlphaNum 10 | b64enc }}"
{{- else }}
mariadb-replication-password: {{ required "A MariaDB Replication Password is required!" .Values.replication.password }}
{{- end }}
{{- end }}
{{- end }}

@ -1,15 +0,0 @@
{{- if and .Values.replication.enabled .Values.slave.config }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "slave.fullname" . }}
labels:
app: {{ template "mariadb.name" . }}
component: "slave"
chart: {{ template "mariadb.chart" . }}
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
data:
my.cnf: |-
{{ .Values.slave.config | indent 4 }}
{{- end }}

@ -1,193 +0,0 @@
{{- if .Values.replication.enabled }}
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: {{ template "slave.fullname" . }}
labels:
app: "{{ template "mariadb.name" . }}"
chart: {{ template "mariadb.chart" . }}
component: "slave"
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
spec:
serviceName: "{{ template "slave.fullname" . }}"
replicas: {{ .Values.slave.replicas }}
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: "{{ template "mariadb.name" . }}"
component: "slave"
release: "{{ .Release.Name }}"
chart: {{ template "mariadb.chart" . }}
spec:
securityContext:
runAsUser: 1001
fsGroup: 1001
{{- if eq .Values.slave.antiAffinity "hard" }}
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: "kubernetes.io/hostname"
labelSelector:
matchLabels:
app: "{{ template "mariadb.name" . }}"
release: "{{ .Release.Name }}"
{{- else if eq .Values.slave.antiAffinity "soft" }}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: "{{ template "mariadb.name" . }}"
release: "{{ .Release.Name }}"
{{- end }}
{{- if .Values.image.pullSecrets }}
imagePullSecrets:
{{- range .Values.image.pullSecrets }}
- name: {{ . }}
{{- end}}
{{- end }}
containers:
- name: "mariadb"
image: {{ template "mariadb.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy | quote }}
env:
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-root-password
{{- if .Values.db.user }}
- name: MARIADB_USER
value: "{{ .Values.db.user }}"
- name: MARIADB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-password
{{- end }}
- name: MARIADB_DATABASE
value: "{{ .Values.db.name }}"
- name: MARIADB_REPLICATION_MODE
value: "slave"
- name: MARIADB_MASTER_HOST
value: {{ template "mariadb.fullname" . }}
- name: MARIADB_MASTER_PORT
value: "3306"
- name: MARIADB_MASTER_USER
value: "root"
- name: MARIADB_MASTER_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-root-password
- name: MARIADB_REPLICATION_USER
value: "{{ .Values.replication.user }}"
- name: MARIADB_REPLICATION_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-replication-password
ports:
- name: mysql
containerPort: 3306
{{- if .Values.slave.livenessProbe.enabled }}
livenessProbe:
exec:
command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"]
initialDelaySeconds: {{ .Values.slave.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.slave.livenessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.slave.livenessProbe.timeoutSeconds }}
successThreshold: {{ .Values.slave.livenessProbe.successThreshold }}
failureThreshold: {{ .Values.slave.livenessProbe.failureThreshold }}
{{- end }}
{{- if .Values.slave.readinessProbe.enabled }}
readinessProbe:
exec:
command: ["sh", "-c", "exec mysqladmin status -uroot -p$MARIADB_ROOT_PASSWORD"]
initialDelaySeconds: {{ .Values.slave.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.slave.readinessProbe.periodSeconds }}
timeoutSeconds: {{ .Values.slave.readinessProbe.timeoutSeconds }}
successThreshold: {{ .Values.slave.readinessProbe.successThreshold }}
failureThreshold: {{ .Values.slave.readinessProbe.failureThreshold }}
{{- end }}
resources:
{{ toYaml .Values.slave.resources | indent 10 }}
volumeMounts:
- name: data
mountPath: /bitnami/mariadb
{{- if .Values.slave.config }}
- name: config
mountPath: /opt/bitnami/mariadb/conf/my.cnf
subPath: my.cnf
{{- end }}
{{- if .Values.metrics.enabled }}
- name: metrics
image: {{ template "metrics.image" . }}
imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }}
env:
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-root-password
command: [ 'sh', '-c', 'DATA_SOURCE_NAME="root:$MARIADB_ROOT_PASSWORD@(localhost:3306)/" /bin/mysqld_exporter' ]
ports:
- name: metrics
containerPort: 9104
livenessProbe:
httpGet:
path: /metrics
port: metrics
initialDelaySeconds: 15
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /metrics
port: metrics
initialDelaySeconds: 5
timeoutSeconds: 1
resources:
{{ toYaml .Values.metrics.resources | indent 10 }}
{{- end }}
volumes:
{{- if .Values.slave.config }}
- name: config
configMap:
name: {{ template "slave.fullname" . }}
{{- end }}
{{- if .Values.slave.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
labels:
app: "{{ template "mariadb.name" . }}"
chart: {{ template "mariadb.chart" . }}
component: "slave"
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
spec:
accessModes:
{{- range .Values.slave.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.slave.persistence.size | quote }}
{{- if .Values.slave.persistence.storageClass }}
{{- if (eq "-" .Values.slave.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.slave.persistence.storageClass | quote }}
{{- end }}
{{- end }}
{{- else }}
- name: "data"
emptyDir: {}
{{- end }}
{{- end }}

@ -1,31 +0,0 @@
{{- if .Values.replication.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ template "slave.fullname" . }}
labels:
app: "{{ template "mariadb.name" . }}"
chart: {{ template "mariadb.chart" . }}
component: "slave"
release: {{ .Release.Name | quote }}
heritage: {{ .Release.Service | quote }}
{{- if .Values.metrics.enabled }}
annotations:
{{ toYaml .Values.metrics.annotations | indent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- name: mysql
port: {{ .Values.service.port }}
targetPort: mysql
{{- if .Values.metrics.enabled }}
- name: metrics
port: 9104
targetPort: metrics
{{- end }}
selector:
app: "{{ template "mariadb.name" . }}"
component: "slave"
release: "{{ .Release.Name }}"
{{- end }}

@ -1,44 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ template "mariadb.fullname" . }}-test-{{ randAlphaNum 5 | lower }}"
annotations:
"helm.sh/hook": test-success
spec:
initContainers:
- name: "test-framework"
image: "dduportal/bats:0.4.0"
command:
- "bash"
- "-c"
- |
set -ex
# copy bats to tools dir
cp -R /usr/local/libexec/ /tools/bats/
volumeMounts:
- mountPath: /tools
name: tools
containers:
- name: mariadb-test
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy | quote }}
command: ["/tools/bats/bats", "-t", "/tests/run.sh"]
env:
- name: MARIADB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "mariadb.fullname" . }}
key: mariadb-root-password
volumeMounts:
- mountPath: /tests
name: tests
readOnly: true
- mountPath: /tools
name: tools
volumes:
- name: tests
configMap:
name: {{ template "mariadb.fullname" . }}-tests
- name: tools
emptyDir: {}
restartPolicy: Never

@ -1,9 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "mariadb.fullname" . }}-tests
data:
run.sh: |-
@test "Testing MariaDB is accessible" {
mysql -h {{ template "mariadb.fullname" . }} -uroot -p$MARIADB_ROOT_PASSWORD -e 'show databases;'
}

@ -1,233 +0,0 @@
## Bitnami MariaDB image
## ref: https://hub.docker.com/r/bitnami/mariadb/tags/
##
image:
registry: docker.io
repository: bitnami/mariadb
tag: 10.1.34-debian-9
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
##
# pullSecrets:
# - myRegistrKeySecretName
service:
## Kubernetes service type
type: ClusterIP
port: 3306
rootUser:
## MariaDB admin password
## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-the-root-password-on-first-run
##
password:
## Use existing secret (ignores root, db and replication passwords)
# existingSecret:
##
## Option to force users to specify a password. That is required for 'helm upgrade' to work properly.
## If it is not force, a random password will be generated.
forcePassword: false
db:
## MariaDB username and password
## ref: https://github.com/bitnami/bitnami-docker-mariadb#creating-a-database-user-on-first-run
##
user:
password:
## Password is ignored if existingSecret is specified.
## Database to create
## ref: https://github.com/bitnami/bitnami-docker-mariadb#creating-a-database-on-first-run
##
name: my_database
## Option to force users to specify a password. That is required for 'helm upgrade' to work properly.
## If it is not force, a random password will be generated.
forcePassword: false
replication:
## Enable replication. This enables the creation of replicas of MariaDB. If false, only a
## master deployment would be created
enabled: true
##
## MariaDB replication user
## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-up-a-replication-cluster
##
user: replicator
## MariaDB replication user password
## ref: https://github.com/bitnami/bitnami-docker-mariadb#setting-up-a-replication-cluster
##
password:
## Password is ignored if existingSecret is specified.
##
## Option to force users to specify a password. That is required for 'helm upgrade' to work properly.
## If it is not force, a random password will be generated.
forcePassword: false
master:
antiAffinity: soft
## Enable persistence using Persistent Volume Claims
## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
##
persistence:
## If true, use a Persistent Volume Claim, If false, use emptyDir
##
enabled: true
## Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
## Persistent Volume Claim annotations
##
annotations:
## Persistent Volume Access Mode
##
accessModes:
- ReadWriteOnce
## Persistent Volume size
##
size: 8Gi
##
## Configure MySQL with a custom my.cnf file
## ref: https://mysql.com/kb/en/mysql/configuring-mysql-with-mycnf/#example-of-configuration-file
##
config: |-
[mysqld]
skip-name-resolve
explicit_defaults_for_timestamp
basedir=/opt/bitnami/mariadb
port=3306
socket=/opt/bitnami/mariadb/tmp/mysql.sock
tmpdir=/opt/bitnami/mariadb/tmp
max_allowed_packet=16M
bind-address=0.0.0.0
pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid
log-error=/opt/bitnami/mariadb/logs/mysqld.log
character-set-server=UTF8
collation-server=utf8_general_ci
[client]
port=3306
socket=/opt/bitnami/mariadb/tmp/mysql.sock
default-character-set=UTF8
[manager]
port=3306
socket=/opt/bitnami/mariadb/tmp/mysql.sock
pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid
## Configure master resource requests and limits
## ref: http://kubernetes.io/docs/user-guide/compute-resources/
##
resources: {}
livenessProbe:
enabled: true
##
## Initializing the database could take some time
initialDelaySeconds: 120
##
## Default Kubernetes values
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
readinessProbe:
enabled: true
initialDelaySeconds: 15
##
## Default Kubernetes values
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
slave:
replicas: 1
antiAffinity: soft
persistence:
## If true, use a Persistent Volume Claim, If false, use emptyDir
##
enabled: true
# storageClass: "-"
annotations:
accessModes:
- ReadWriteOnce
## Persistent Volume size
##
size: 8Gi
##
## Configure MySQL slave with a custom my.cnf file
## ref: https://mysql.com/kb/en/mysql/configuring-mysql-with-mycnf/#example-of-configuration-file
##
config: |-
[mysqld]
skip-name-resolve
explicit_defaults_for_timestamp
basedir=/opt/bitnami/mariadb
port=3306
socket=/opt/bitnami/mariadb/tmp/mysql.sock
tmpdir=/opt/bitnami/mariadb/tmp
max_allowed_packet=16M
bind-address=0.0.0.0
pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid
log-error=/opt/bitnami/mariadb/logs/mysqld.log
character-set-server=UTF8
collation-server=utf8_general_ci
[client]
port=3306
socket=/opt/bitnami/mariadb/tmp/mysql.sock
default-character-set=UTF8
[manager]
port=3306
socket=/opt/bitnami/mariadb/tmp/mysql.sock
pid-file=/opt/bitnami/mariadb/tmp/mysqld.pid
##
## Configure slave resource requests and limits
## ref: http://kubernetes.io/docs/user-guide/compute-resources/
##
resources: {}
livenessProbe:
enabled: true
##
## Initializing the database could take some time
initialDelaySeconds: 120
##
## Default Kubernetes values
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
readinessProbe:
enabled: true
initialDelaySeconds: 15
##
## Default Kubernetes values
periodSeconds: 10
timeoutSeconds: 1
successThreshold: 1
failureThreshold: 3
metrics:
enabled: false
image:
registry: docker.io
repository: prom/mysqld-exporter
tag: v0.10.0
pullPolicy: IfNotPresent
resources: {}
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9104"

@ -1,6 +0,0 @@
dependencies:
- name: mariadb
repository: https://charts.helm.sh/stable/
version: 4.3.1
digest: sha256:82a0e5374376169d2ecf7d452c18a2ed93507f5d17c3393a1457f9ffad7e9b26
generated: 2018-08-02T22:07:51.905271776Z

@ -1,7 +0,0 @@
dependencies:
- name: mariadb
version: 4.x.x
repository: https://charts.helm.sh/stable/
condition: mariadb.enabled
tags:
- wordpress-database

@ -1,254 +0,0 @@
## Bitnami WordPress image version
## ref: https://hub.docker.com/r/bitnami/wordpress/tags/
##
image:
registry: docker.io
repository: bitnami/wordpress
tag: 4.9.8-debian-9
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
##
# pullSecrets:
# - myRegistrKeySecretName
## User of the application
## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables
##
wordpressUsername: user
## Application password
## Defaults to a random 10-character alphanumeric string if not set
## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables
##
# wordpressPassword:
## Admin email
## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables
##
wordpressEmail: user@example.com
## First name
## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables
##
wordpressFirstName: FirstName
## Last name
## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables
##
wordpressLastName: LastName
## Blog name
## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables
##
wordpressBlogName: User's Blog!
## Table prefix
## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables
##
wordpressTablePrefix: wp_
## Set to `yes` to allow the container to be started with blank passwords
## ref: https://github.com/bitnami/bitnami-docker-wordpress#environment-variables
allowEmptyPassword: yes
## SMTP mail delivery configuration
## ref: https://github.com/bitnami/bitnami-docker-wordpress/#smtp-configuration
##
# smtpHost:
# smtpPort:
# smtpUser:
# smtpPassword:
# smtpUsername:
# smtpProtocol:
replicaCount: 1
externalDatabase:
## All of these values are only used when mariadb.enabled is set to false
## Database host
host: localhost
## non-root Username for Wordpress Database
user: bn_wordpress
## Database password
password: ""
## Database name
database: bitnami_wordpress
## Database port number
port: 3306
##
## MariaDB chart configuration
##
mariadb:
## Whether to deploy a mariadb server to satisfy the applications database requirements. To use an external database set this to false and configure the externalDatabase parameters
enabled: true
## Disable MariaDB replication
replication:
enabled: false
## Create a database and a database user
## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#creating-a-database-user-on-first-run
##
db:
name: bitnami_wordpress
user: bn_wordpress
## If the password is not specified, mariadb will generates a random password
##
# password:
## MariaDB admin password
## ref: https://github.com/bitnami/bitnami-docker-mariadb/blob/master/README.md#setting-the-root-password-on-first-run
##
# rootUser:
# password:
## Enable persistence using Persistent Volume Claims
## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
##
master:
persistence:
enabled: true
## mariadb data Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
accessMode: ReadWriteOnce
size: 8Gi
## Kubernetes configuration
## For minikube, set this to NodePort, elsewhere use LoadBalancer or ClusterIP
##
serviceType: LoadBalancer
##
## serviceType: NodePort
## nodePorts:
## http: <to set explicitly, choose port between 30000-32767>
## https: <to set explicitly, choose port between 30000-32767>
nodePorts:
http: ""
https: ""
## Enable client source IP preservation
## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip
##
serviceExternalTrafficPolicy: Cluster
## Allow health checks to be pointed at the https port
healthcheckHttps: false
## Configure extra options for liveness and readiness probes
## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes)
livenessProbe:
initialDelaySeconds: 120
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
readinessProbe:
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
successThreshold: 1
## Configure the ingress resource that allows you to access the
## Wordpress installation. Set up the URL
## ref: http://kubernetes.io/docs/user-guide/ingress/
##
ingress:
## Set to true to enable ingress record generation
enabled: false
## The list of hostnames to be covered with this ingress record.
## Most likely this will be just one host, but in the event more hosts are needed, this is an array
hosts:
- name: wordpress.local
## Set this to true in order to enable TLS on the ingress record
## A side effect of this will be that the backend wordpress service will be connected at port 443
tls: false
## If TLS is set to true, you must declare what secret will store the key/certificate for TLS
tlsSecret: wordpress.local-tls
## Ingress annotations done as key:value pairs
## If you're using kube-lego, you will want to add:
## kubernetes.io/tls-acme: true
##
## For a full list of possible ingress annotations, please see
## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/annotations.md
##
## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set
annotations:
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: true
secrets:
## If you're providing your own certificates, please use this to add the certificates as secrets
## key and certificate should start with -----BEGIN CERTIFICATE----- or
## -----BEGIN RSA PRIVATE KEY-----
##
## name should line up with a tlsSecret set further up
## If you're using kube-lego, this is unneeded, as it will create the secret for you if it is not set
##
## It is also possible to create and manage the certificates outside of this helm chart
## Please see README.md for more information
# - name: wordpress.local-tls
# key:
# certificate:
## Enable persistence using Persistent Volume Claims
## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
##
persistence:
enabled: true
## wordpress data Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
##
## If you want to reuse an existing claim, you can pass the name of the PVC using
## the existingClaim variable
# existingClaim: your-claim
accessMode: ReadWriteOnce
size: 10Gi
## Configure resource requests and limits
## ref: http://kubernetes.io/docs/user-guide/compute-resources/
##
resources:
requests:
memory: 512Mi
cpu: 300m
## Node labels for pod assignment
## Ref: https://kubernetes.io/docs/user-guide/node-selection/
##
nodeSelector: {}
## Tolerations for pod assignment
## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
##
tolerations: []
## Affinity for pod assignment
## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
##
affinity: {}

@ -1,4 +0,0 @@
apiVersion: v1
description: A Helm chart for Kubernetes
name: decompressedchart
version: 0.1.0

@ -1,4 +0,0 @@
# Default values for decompressedchart.
# This is a YAML-formatted file.
# Declare name/value pairs to be passed into your templates.
name: my-decompressed-chart

@ -1,4 +0,0 @@
apiVersion: v1
name: multiplecharts-lint-chart-1
version: "1"
icon: ""

@ -1,6 +0,0 @@
apiVersion: v1
metadata:
name: multicharttest-chart1-configmap
data:
dat: |
{{ .Values.config | indent 4 }}

@ -1,4 +0,0 @@
apiVersion: v1
name: multiplecharts-lint-chart-2
version: "1"
icon: ""

@ -1,5 +0,0 @@
apiVersion: v1
metadata:
name: multicharttest-chart2-configmap
data:
{{ toYaml .Values.config | indent 4 }}

@ -1,3 +0,0 @@
NAME VERSION REPOSITORY STATUS
mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ unpacked

@ -1,3 +0,0 @@
NAME VERSION REPOSITORY STATUS
mariadb 4.x.x https://charts.helm.sh/stable/ ok

@ -1,3 +0,0 @@
NAME VERSION REPOSITORY STATUS
mariadb 4.x.x https://charts.helm.sh/stable/ missing

@ -1,3 +0,0 @@
NAME VERSION REPOSITORY STATUS
mariadb 4.x.x https://kubernetes-charts.storage.googleapis.com/ unpacked

@ -1,3 +0,0 @@
NAME VERSION REPOSITORY STATUS
mariadb 4.x.x https://charts.helm.sh/stable/ unpacked

@ -1,25 +0,0 @@
---
# Source: hello/templates/rbac
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: schedule-agents
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/log"]
verbs: ["*"]
---
# Source: hello/templates/rbac
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: schedule-agents
namespace: spaced
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: schedule-agents
subjects:
- kind: ServiceAccount
name: schedule-agents
namespace: spaced

@ -1,226 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"strings"
"time"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
"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
Wait 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)
}
deletedResources, kept, errs := u.deleteRelease(rel)
if errs != nil {
u.cfg.Log("uninstall: Failed to delete release: %s", errs)
return nil, errors.Errorf("failed to delete release: %s", name)
}
if kept != "" {
kept = "These resources were kept due to the resource policy:\n" + kept
}
res.Info = kept
if u.Wait {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceExt); ok {
if err := kubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil {
errs = append(errs, err)
}
}
}
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 list of delete resources and manifests that were kept in the deletion process
func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, string, []error) {
var errs []error
caps, err := u.cfg.getCapabilities()
if err != nil {
return nil, 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 nil, 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 nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
}
if len(resources) > 0 {
_, errs = u.cfg.KubeClient.Delete(resources)
}
return resources, kept, errs
}

@ -1,97 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
)
func uninstallAction(t *testing.T) *Uninstall {
config := actionConfigFixture(t)
unAction := NewUninstall(config)
return unAction
}
func TestUninstallRelease_deleteRelease(t *testing.T) {
is := assert.New(t)
unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.KeepHistory = true
rel := releaseStub()
rel.Name = "keep-secret"
rel.Manifest = `{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "secret",
"annotations": {
"helm.sh/resource-policy": "keep"
}
},
"type": "Opaque",
"data": {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
res, err := unAction.Run(rel.Name)
is.NoError(err)
expected := `These resources were kept due to the resource policy:
[Secret] secret
`
is.Contains(res.Info, expected)
}
func TestUninstallRelease_Wait(t *testing.T) {
is := assert.New(t)
unAction := uninstallAction(t)
unAction.DisableHooks = true
unAction.DryRun = false
unAction.Wait = true
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Manifest = `{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "secret"
},
"type": "Opaque",
"data": {
"password": "password"
}
}`
unAction.cfg.Releases.Create(rel)
failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("U timed out")
unAction.cfg.KubeClient = failer
res, err := unAction.Run(rel.Name)
is.Error(err)
is.Contains(err.Error(), "U timed out")
is.Equal(res.Release.Info.Status, release.StatusUninstalled)
}

@ -1,574 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"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
// Get missing dependencies
DependencyUpdate bool
// Lock to control raceconditions when the process receives a SIGTERM
Lock sync.Mutex
}
type resultMessage struct {
r *release.Release
e error
}
// NewUpgrade creates a new Upgrade object with the given configuration.
func NewUpgrade(cfg *Configuration) *Upgrade {
up := &Upgrade{
cfg: cfg,
}
up.ChartPathOptions.registryClient = cfg.RegistryClient
return up
}
// Run executes the upgrade on the given release.
func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
ctx := context.Background()
return u.RunWithContext(ctx, name, chart, vals)
}
// RunWithContext executes the upgrade on the given release with context.
func (u *Upgrade) RunWithContext(ctx context.Context, 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(ctx, 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(ctx context.Context, 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
}
rChan := make(chan resultMessage)
ctxChan := make(chan resultMessage)
doneChan := make(chan interface{})
defer close(doneChan)
go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease)
go u.handleContext(ctx, doneChan, ctxChan, upgradedRelease)
select {
case result := <-rChan:
return result.r, result.e
case result := <-ctxChan:
return result.r, result.e
}
}
// Function used to lock the Mutex, this is important for the case when the atomic flag is set.
// In that case the upgrade will finish before the rollback is finished so it is necessary to wait for the rollback to finish.
// The rollback will be trigger by the function failRelease
func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Release, created kube.ResourceList, err error) {
u.Lock.Lock()
if err != nil {
rel, err = u.failRelease(rel, created, err)
}
c <- resultMessage{r: rel, e: err}
u.Lock.Unlock()
}
// Setup listener for SIGINT and SIGTERM
func (u *Upgrade) handleContext(ctx context.Context, done chan interface{}, c chan<- resultMessage, upgradedRelease *release.Release) {
select {
case <-ctx.Done():
err := ctx.Err()
// when the atomic flag is set the ongoing release finish first and doesn't give time for the rollback happens.
u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, err)
case <-done:
return
}
}
func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) {
// pre-upgrade hooks
if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil {
u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err))
return
}
} 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)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
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 {
u.cfg.Log(
"waiting for release %s resources (created: %d updated: %d deleted: %d)",
upgradedRelease.Name, len(results.Created), len(results.Updated), len(results.Deleted))
if u.WaitForJobs {
if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
} else {
if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease)
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return
}
}
}
// post-upgrade hooks
if !u.DisableHooks {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil {
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err))
return
}
}
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"
}
u.reportToPerformUpgrade(c, upgradedRelease, nil, 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)
}

@ -1,390 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"context"
"fmt"
"testing"
"time"
"helm.sh/helm/v3/pkg/chart"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
func upgradeAction(t *testing.T) *Upgrade {
config := actionConfigFixture(t)
upAction := NewUpgrade(config)
upAction.Namespace = "spaced"
return upAction
}
func TestUpgradeRelease_Success(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "previous-release"
rel.Info.Status = release.StatusDeployed
req.NoError(upAction.cfg.Releases.Create(rel))
upAction.Wait = true
vals := map[string]interface{}{}
ctx, done := context.WithCancel(context.Background())
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
done()
req.NoError(err)
is.Equal(res.Info.Status, release.StatusDeployed)
// Detecting previous bug where context termination after successful release
// caused release to fail.
time.Sleep(time.Millisecond * 100)
lastRelease, err := upAction.cfg.Releases.Last(rel.Name)
req.NoError(err)
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
}
func TestUpgradeRelease_Wait(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
upAction.cfg.KubeClient = failer
upAction.Wait = true
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestUpgradeRelease_WaitForJobs(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
upAction.cfg.KubeClient = failer
upAction.Wait = true
upAction.WaitForJobs = true
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestUpgradeRelease_CleanupOnFail(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitError = fmt.Errorf("I timed out")
failer.DeleteError = fmt.Errorf("I tried to delete nil")
upAction.cfg.KubeClient = failer
upAction.Wait = true
upAction.CleanupOnFail = true
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
is.NotContains(err.Error(), "unable to cleanup resources")
is.Contains(res.Info.Description, "I timed out")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestUpgradeRelease_Atomic(t *testing.T) {
is := assert.New(t)
req := require.New(t)
t.Run("atomic rollback succeeds", func(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
// We can't make Update error because then the rollback won't work
failer.WatchUntilReadyError = fmt.Errorf("arming key removed")
upAction.cfg.KubeClient = failer
upAction.Atomic = true
vals := map[string]interface{}{}
res, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(err.Error(), "arming key removed")
is.Contains(err.Error(), "atomic")
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3)
is.NoError(err)
// Should have rolled back to the previous
is.Equal(updatedRes.Info.Status, release.StatusDeployed)
})
t.Run("atomic uninstall fails", func(t *testing.T) {
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "fallout"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.UpdateError = fmt.Errorf("update fail")
upAction.cfg.KubeClient = failer
upAction.Atomic = true
vals := map[string]interface{}{}
_, err := upAction.Run(rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(err.Error(), "update fail")
is.Contains(err.Error(), "an error occurred while rolling back the release")
})
}
func TestUpgradeRelease_ReuseValues(t *testing.T) {
is := assert.New(t)
t.Run("reuse values should work with values", func(t *testing.T) {
upAction := upgradeAction(t)
existingValues := map[string]interface{}{
"name": "value",
"maxHeapSize": "128m",
"replicas": 2,
}
newValues := map[string]interface{}{
"name": "newValue",
"maxHeapSize": "512m",
"cpu": "12m",
}
expectedValues := map[string]interface{}{
"name": "newValue",
"maxHeapSize": "512m",
"cpu": "12m",
"replicas": 2,
}
rel := releaseStub()
rel.Name = "nuketown"
rel.Info.Status = release.StatusDeployed
rel.Config = existingValues
err := upAction.cfg.Releases.Create(rel)
is.NoError(err)
upAction.ReuseValues = true
// setting newValues and upgrading
res, err := upAction.Run(rel.Name, buildChart(), newValues)
is.NoError(err)
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2)
is.NoError(err)
if updatedRes == nil {
is.Fail("Updated Release is nil")
return
}
is.Equal(release.StatusDeployed, updatedRes.Info.Status)
is.Equal(expectedValues, updatedRes.Config)
})
t.Run("reuse values should not install disabled charts", func(t *testing.T) {
upAction := upgradeAction(t)
chartDefaultValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": true,
},
}
dependency := chart.Dependency{
Name: "subchart",
Version: "0.1.0",
Repository: "http://some-repo.com",
Condition: "subchart.enabled",
}
sampleChart := buildChart(
withName("sample"),
withValues(chartDefaultValues),
withMetadataDependency(dependency),
)
now := helmtime.Now()
existingValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": false,
},
}
rel := &release.Release{
Name: "nuketown",
Info: &release.Info{
FirstDeployed: now,
LastDeployed: now,
Status: release.StatusDeployed,
Description: "Named Release Stub",
},
Chart: sampleChart,
Config: existingValues,
Version: 1,
}
err := upAction.cfg.Releases.Create(rel)
is.NoError(err)
upAction.ReuseValues = true
sampleChartWithSubChart := buildChart(
withName(sampleChart.Name()),
withValues(sampleChart.Values),
withDependency(withName("subchart")),
withMetadataDependency(dependency),
)
// reusing values and upgrading
res, err := upAction.Run(rel.Name, sampleChartWithSubChart, map[string]interface{}{})
is.NoError(err)
// Now get the upgraded release
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2)
is.NoError(err)
if updatedRes == nil {
is.Fail("Updated Release is nil")
return
}
is.Equal(release.StatusDeployed, updatedRes.Info.Status)
is.Equal(0, len(updatedRes.Chart.Dependencies()), "expected 0 dependencies")
expectedValues := map[string]interface{}{
"subchart": map[string]interface{}{
"enabled": false,
},
}
is.Equal(expectedValues, updatedRes.Config)
})
}
func TestUpgradeRelease_Pending(t *testing.T) {
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "come-fail-away"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
rel2 := releaseStub()
rel2.Name = "come-fail-away"
rel2.Info.Status = release.StatusPendingUpgrade
rel2.Version = 2
upAction.cfg.Releases.Create(rel2)
vals := map[string]interface{}{}
_, err := upAction.Run(rel.Name, buildChart(), vals)
req.Contains(err.Error(), "progress", err)
}
func TestUpgradeRelease_Interrupted_Wait(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "interrupted-release"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 10 * time.Second
upAction.cfg.KubeClient = failer
upAction.Wait = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled")
is.Equal(res.Info.Status, release.StatusFailed)
}
func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) {
is := assert.New(t)
req := require.New(t)
upAction := upgradeAction(t)
rel := releaseStub()
rel.Name = "interrupted-release"
rel.Info.Status = release.StatusDeployed
upAction.cfg.Releases.Create(rel)
failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WaitDuration = 5 * time.Second
upAction.cfg.KubeClient = failer
upAction.Atomic = true
vals := map[string]interface{}{}
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(time.Second, cancel)
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals)
req.Error(err)
is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to atomic being set: context canceled")
// Now make sure it is actually upgraded
updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3)
is.NoError(err)
// Should have rolled back to the previous
is.Equal(updatedRes.Info.Status, release.StatusDeployed)
}

@ -1,184 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"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.Wrapf(err, "could not get information about the resource %s", resourceString(info))
}
// 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
}

@ -1,123 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"testing"
"helm.sh/helm/v3/pkg/kube"
appsv1 "k8s.io/api/apps/v1"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/resource"
)
func newDeploymentResource(name, namespace string) *resource.Info {
return &resource.Info{
Name: name,
Mapping: &meta.RESTMapping{
Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"},
GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
},
Object: &appsv1.Deployment{
ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace,
},
},
}
}
func TestCheckOwnership(t *testing.T) {
deployFoo := newDeploymentResource("foo", "ns-a")
// Verify that a resource that lacks labels/annotations is not owned
err := checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by": must be set to "Helm"; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`)
// Set managed by label and verify annotation error message
_ = accessor.SetLabels(deployFoo.Object, map[string]string{
appManagedByLabel: appManagedByHelm,
})
err = checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`)
// Set only the release name annotation and verify missing release namespace error message
_ = accessor.SetAnnotations(deployFoo.Object, map[string]string{
helmReleaseNameAnnotation: "rel-a",
})
err = checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`)
// Set both release name and namespace annotations and verify no ownership errors
_ = accessor.SetAnnotations(deployFoo.Object, map[string]string{
helmReleaseNameAnnotation: "rel-a",
helmReleaseNamespaceAnnotation: "ns-a",
})
err = checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.NoError(t, err)
// Verify ownership error for wrong release name
err = checkOwnership(deployFoo.Object, "rel-b", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-name" must equal "rel-b": current value is "rel-a"`)
// Verify ownership error for wrong release namespace
err = checkOwnership(deployFoo.Object, "rel-a", "ns-b")
assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-namespace" must equal "ns-b": current value is "ns-a"`)
// Verify ownership error for wrong manager label
_ = accessor.SetLabels(deployFoo.Object, map[string]string{
appManagedByLabel: "helm",
})
err = checkOwnership(deployFoo.Object, "rel-a", "ns-a")
assert.EqualError(t, err, `invalid ownership metadata; label validation error: key "app.kubernetes.io/managed-by" must equal "Helm": current value is "helm"`)
}
func TestSetMetadataVisitor(t *testing.T) {
var (
err error
deployFoo = newDeploymentResource("foo", "ns-a")
deployBar = newDeploymentResource("bar", "ns-a-system")
resources = kube.ResourceList{deployFoo, deployBar}
)
// Set release tracking metadata and verify no error
err = resources.Visit(setMetadataVisitor("rel-a", "ns-a", true))
assert.NoError(t, err)
// Verify that release "b" cannot take ownership of "a"
err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false))
assert.Error(t, err)
// Force release "b" to take ownership
err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", true))
assert.NoError(t, err)
// Check that there is now no ownership error when setting metadata without force
err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false))
assert.NoError(t, err)
// Add a new resource that is missing ownership metadata and verify error
resources.Append(newDeploymentResource("baz", "default"))
err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false))
assert.Error(t, err)
assert.Contains(t, err.Error(), `Deployment "baz" in namespace "" cannot be owned`)
}

@ -1,59 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"fmt"
"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
}

@ -1,173 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package chart
import (
"path/filepath"
"regexp"
"strings"
)
// APIVersionV1 is the API version number for version 1.
const APIVersionV1 = "v1"
// APIVersionV2 is the API version number for version 2.
const APIVersionV2 = "v2"
// aliasNameFormat defines the characters that are legal in an alias name.
var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
// Chart is a helm package that contains metadata, a default config, zero or more
// optionally parameterizable templates, and zero or more charts (dependencies).
type Chart struct {
// Raw contains the raw contents of the files originally contained in the chart archive.
//
// This should not be used except in special cases like `helm show values`,
// where we want to display the raw values, comments and all.
Raw []*File `json:"-"`
// Metadata is the contents of the Chartfile.
Metadata *Metadata `json:"metadata"`
// Lock is the contents of Chart.lock.
Lock *Lock `json:"lock"`
// Templates for this chart.
Templates []*File `json:"templates"`
// Values are default config for this chart.
Values map[string]interface{} `json:"values"`
// Schema is an optional JSON schema for imposing structure on Values
Schema []byte `json:"schema"`
// Files are miscellaneous files in a chart archive,
// e.g. README, LICENSE, etc.
Files []*File `json:"files"`
parent *Chart
dependencies []*Chart
}
type CRD struct {
// Name is the File.Name for the crd file
Name string
// Filename is the File obj Name including (sub-)chart.ChartFullPath
Filename string
// File is the File obj for the crd
File *File
}
// SetDependencies replaces the chart dependencies.
func (ch *Chart) SetDependencies(charts ...*Chart) {
ch.dependencies = nil
ch.AddDependency(charts...)
}
// Name returns the name of the chart.
func (ch *Chart) Name() string {
if ch.Metadata == nil {
return ""
}
return ch.Metadata.Name
}
// AddDependency determines if the chart is a subchart.
func (ch *Chart) AddDependency(charts ...*Chart) {
for i, x := range charts {
charts[i].parent = ch
ch.dependencies = append(ch.dependencies, x)
}
}
// Root finds the root chart.
func (ch *Chart) Root() *Chart {
if ch.IsRoot() {
return ch
}
return ch.Parent().Root()
}
// Dependencies are the charts that this chart depends on.
func (ch *Chart) Dependencies() []*Chart { return ch.dependencies }
// IsRoot determines if the chart is the root chart.
func (ch *Chart) IsRoot() bool { return ch.parent == nil }
// Parent returns a subchart's parent chart.
func (ch *Chart) Parent() *Chart { return ch.parent }
// ChartPath returns the full path to this chart in dot notation.
func (ch *Chart) ChartPath() string {
if !ch.IsRoot() {
return ch.Parent().ChartPath() + "." + ch.Name()
}
return ch.Name()
}
// ChartFullPath returns the full path to this chart.
func (ch *Chart) ChartFullPath() string {
if !ch.IsRoot() {
return ch.Parent().ChartFullPath() + "/charts/" + ch.Name()
}
return ch.Name()
}
// Validate validates the metadata.
func (ch *Chart) Validate() error {
return ch.Metadata.Validate()
}
// AppVersion returns the appversion of the chart.
func (ch *Chart) AppVersion() string {
if ch.Metadata == nil {
return ""
}
return ch.Metadata.AppVersion
}
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
// Deprecated: use CRDObjects()
func (ch *Chart) CRDs() []*File {
files := []*File{}
// Find all resources in the crds/ directory
for _, f := range ch.Files {
if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
files = append(files, f)
}
}
// Get CRDs from dependencies, too.
for _, dep := range ch.Dependencies() {
files = append(files, dep.CRDs()...)
}
return files
}
// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts
func (ch *Chart) CRDObjects() []CRD {
crds := []CRD{}
// Find all resources in the crds/ directory
for _, f := range ch.Files {
if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) {
mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f}
crds = append(crds, mycrd)
}
}
// Get CRDs from dependencies, too.
for _, dep := range ch.Dependencies() {
crds = append(crds, dep.CRDObjects()...)
}
return crds
}
func hasManifestExtension(fname string) bool {
ext := filepath.Ext(fname)
return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json")
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save