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