mirror of https://github.com/helm/helm
Signed-off-by: HARPHUNA <109001160+HARPHUNA@users.noreply.github.com>pull/11491/head
parent
b35ff17cd2
commit
7ff00a3099
@ -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
|
|
Binary file not shown.
@ -1,2 +0,0 @@
|
|||||||
name: chart-with-compressed-dependencies
|
|
||||||
version: 2.1.8
|
|
Binary file not shown.
@ -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 +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,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"
|
|
Binary file not shown.
@ -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 +0,0 @@
|
|||||||
.git
|
|
@ -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,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 +0,0 @@
|
|||||||
Placeholder.
|
|
@ -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: {}
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 +0,0 @@
|
|||||||
config: "Test"
|
|
@ -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,2 +0,0 @@
|
|||||||
config:
|
|
||||||
test: "Test"
|
|
Binary file not shown.
@ -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…
Reference in new issue