feat: kubesphere 4.0 (#6115)

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

---------

Signed-off-by: ci-bot <ci-bot@kubesphere.io>
Co-authored-by: ks-ci-bot <ks-ci-bot@example.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
KubeSphere CI Bot
2024-09-06 11:05:52 +08:00
committed by GitHub
parent b5015ec7b9
commit 447a51f08b
8557 changed files with 546695 additions and 1146174 deletions

View File

@@ -18,7 +18,6 @@ package fileutil
import (
"io"
"io/ioutil"
"os"
"path/filepath"
@@ -28,7 +27,7 @@ import (
// AtomicWriteFile atomically (as atomic as os.Rename allows) writes a file to a
// disk.
func AtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error {
tempFile, err := ioutil.TempFile(filepath.Split(filename))
tempFile, err := os.CreateTemp(filepath.Split(filename))
if err != nil {
return err
}

View File

@@ -1,67 +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 ignore provides tools for writing ignore files (a la .gitignore).
This provides both an ignore parser and a file-aware processor.
The format of ignore files closely follows, but does not exactly match, the
format for .gitignore files (https://git-scm.com/docs/gitignore).
The formatting rules are as follows:
- Parsing is line-by-line
- Empty lines are ignored
- Lines the begin with # (comments) will be ignored
- Leading and trailing spaces are always ignored
- Inline comments are NOT supported ('foo* # Any foo' does not contain a comment)
- There is no support for multi-line patterns
- Shell glob patterns are supported. See Go's "path/filepath".Match
- If a pattern begins with a leading !, the match will be negated.
- If a pattern begins with a leading /, only paths relatively rooted will match.
- If the pattern ends with a trailing /, only directories will match
- If a pattern contains no slashes, file basenames are tested (not paths)
- The pattern sequence "**", while legal in a glob, will cause an error here
(to indicate incompatibility with .gitignore).
Example:
# Match any file named foo.txt
foo.txt
# Match any text file
*.txt
# Match only directories named mydir
mydir/
# Match only text files in the top-level directory
/*.txt
# Match only the file foo.txt in the top-level directory
/foo.txt
# Match any file named ab.txt, ac.txt, or ad.txt
a[b-d].txt
Notable differences from .gitignore:
- The '**' syntax is not supported.
- The globbing library is Go's 'filepath.Match', not fnmatch(3)
- Trailing spaces are always ignored (there is no supported escape sequence)
- The evaluation of escape sequences has not been tested for compatibility
- There is no support for '\!' as a special leading sequence.
*/
package ignore // import "helm.sh/helm/v3/internal/ignore"

View File

@@ -92,9 +92,9 @@ func FindNewReplicaSet(deployment *apps.Deployment, rsList []*apps.ReplicaSet) *
// EqualIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash]
// We ignore pod-template-hash because:
// 1. The hash result would be different upon podTemplateSpec API changes
// (e.g. the addition of a new field will cause the hash code to change)
// 2. The deployment template won't have hash labels
// 1. The hash result would be different upon podTemplateSpec API changes
// (e.g. the addition of a new field will cause the hash code to change)
// 2. The deployment template won't have hash labels
func EqualIgnoreHash(template1, template2 *v1.PodTemplateSpec) bool {
t1Copy := template1.DeepCopy()
t2Copy := template2.DeepCopy()

View File

@@ -19,14 +19,16 @@ package tlsutil
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"os"
"github.com/pkg/errors"
)
// NewClientTLS returns tls.Config appropriate for client auth.
func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) {
config := tls.Config{}
func NewClientTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*tls.Config, error) {
config := tls.Config{
InsecureSkipVerify: insecureSkipTLSverify,
}
if certFile != "" && keyFile != "" {
cert, err := CertFromFilePair(certFile, keyFile)
@@ -52,7 +54,7 @@ func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) {
// Returns an error if the file could not be read, a certificate could not
// be parsed, or if the file does not contain any certificates
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
b, err := ioutil.ReadFile(filename)
b, err := os.ReadFile(filename)
if err != nil {
return nil, errors.Errorf("can't read CA file: %v", filename)
}

View File

@@ -29,7 +29,7 @@ var (
//
// Increment major number for new feature additions and behavioral changes.
// Increment minor number for bug fixes and performance enhancements.
version = "v3.11"
version = "v3.14"
// metadata is extra build time data
metadata = ""

View File

@@ -103,7 +103,7 @@ type Configuration struct {
// 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, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) {
func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) {
hs := []*release.Hook{}
b := bytes.NewBuffer(nil)
@@ -121,12 +121,10 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
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 {
// A `helm template` should not talk to the remote cluster. However, commands with the flag
//`--dry-run` with the value of `false`, `none`, or `server` should try to interact with the cluster.
// It may break in interesting and exotic ways because other data (e.g. discovery) is mocked.
if interactWithRemote && cfg.RESTClientGetter != nil {
restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil {
return hs, b, "", err
@@ -189,13 +187,13 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
if includeCrds {
for _, crd := range ch.CRDObjects() {
if outputDir == "" {
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:]))
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Filename, string(crd.File.Data[:]))
} else {
err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name])
err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Filename])
if err != nil {
return hs, b, "", err
}
fileWritten[crd.Name] = true
fileWritten[crd.Filename] = true
}
}
}

View File

@@ -0,0 +1,69 @@
/*
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 "time"
// GetMetadata is the action for checking a given release's metadata.
//
// It provides the implementation of 'helm get metadata'.
type GetMetadata struct {
cfg *Configuration
Version int
}
type Metadata struct {
Name string `json:"name" yaml:"name"`
Chart string `json:"chart" yaml:"chart"`
Version string `json:"version" yaml:"version"`
AppVersion string `json:"appVersion" yaml:"appVersion"`
Namespace string `json:"namespace" yaml:"namespace"`
Revision int `json:"revision" yaml:"revision"`
Status string `json:"status" yaml:"status"`
DeployedAt string `json:"deployedAt" yaml:"deployedAt"`
}
// NewGetMetadata creates a new GetMetadata object with the given configuration.
func NewGetMetadata(cfg *Configuration) *GetMetadata {
return &GetMetadata{
cfg: cfg,
}
}
// Run executes 'helm get metadata' against the given release.
func (g *GetMetadata) Run(name string) (*Metadata, 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
}
return &Metadata{
Name: rel.Name,
Chart: rel.Chart.Metadata.Name,
Version: rel.Chart.Metadata.Version,
AppVersion: rel.Chart.Metadata.AppVersion,
Namespace: rel.Namespace,
Revision: rel.Version,
Status: rel.Info.Status.String(),
DeployedAt: rel.Info.LastDeployed.Format(time.RFC3339),
}, nil
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
@@ -51,7 +52,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
}
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil {
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation, timeout); err != nil {
return err
}
@@ -88,7 +89,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
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 {
if err := cfg.deleteHookByPolicy(h, release.HookFailed, timeout); err != nil {
return err
}
return err
@@ -99,7 +100,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// 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 {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, timeout); err != nil {
return err
}
}
@@ -120,7 +121,7 @@ func (x hookByWeight) Less(i, j int) bool {
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error {
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy, timeout time.Duration) error {
// Never delete CustomResourceDefinitions; this could cause lots of
// cascading garbage collection.
if h.Kind == "CustomResourceDefinition" {
@@ -135,6 +136,13 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo
if len(errs) > 0 {
return errors.New(joinErrors(errs))
}
//wait for resources until they are deleted to avoid conflicts
if kubeClient, ok := cfg.KubeClient.(kube.InterfaceExt); ok {
if err := kubeClient.WaitForDelete(resources, timeout); err != nil {
return err
}
}
}
return nil
}

View File

@@ -20,7 +20,7 @@ import (
"bytes"
"context"
"fmt"
"io/ioutil"
"io"
"net/url"
"os"
"path"
@@ -34,6 +34,7 @@ import (
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
@@ -72,6 +73,7 @@ type Install struct {
Force bool
CreateNamespace bool
DryRun bool
DryRunOption string
DisableHooks bool
Replace bool
Wait bool
@@ -90,6 +92,7 @@ type Install struct {
SubNotes bool
DisableOpenAPIValidation bool
IncludeCRDs bool
Labels map[string]string
// 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
@@ -113,6 +116,7 @@ type ChartPathOptions struct {
CertFile string // --cert-file
KeyFile string // --key-file
InsecureSkipTLSverify bool // --insecure-skip-verify
PlainHTTP bool // --plain-http
Keyring string // --keyring
Password string // --password
PassCredentialsAll bool // --pass-credentials
@@ -136,6 +140,16 @@ func NewInstall(cfg *Configuration) *Install {
return in
}
// SetRegistryClient sets the registry client for the install action
func (i *Install) SetRegistryClient(registryClient *registry.Client) {
i.ChartPathOptions.registryClient = registryClient
}
// GetRegistryClient get the registry client.
func (i *Install) GetRegistryClient() *registry.Client {
return i.ChartPathOptions.registryClient
}
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{}
@@ -159,22 +173,38 @@ func (i *Install) installCRDs(crds []chart.CRD) error {
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()
// If we have already gathered the capabilities, we need to invalidate
// the cache so that the new CRDs are recognized. This should only be
// the case when an action configuration is reused for multiple actions,
// as otherwise it is later loaded by ourselves when getCapabilities
// is called later on in the installation process.
if i.cfg.Capabilities != nil {
discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient()
if err != nil {
return err
}
i.cfg.Log("Clearing discovery cache")
discoveryClient.Invalidate()
_, _ = discoveryClient.ServerGroups()
}
// Invalidate the REST mapper, since it will not have the new CRDs
// present.
restMapper, err := i.cfg.RESTClientGetter.ToRESTMapper()
if err != nil {
return err
}
if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok {
i.cfg.Log("Clearing REST mapper cache")
resettable.Reset()
}
}
return nil
}
@@ -189,6 +219,9 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
}
// Run executes the installation with Context
//
// When the task is cancelled through ctx, the function returns and the install
// proceeds in the background.
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 {
@@ -201,15 +234,20 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
return nil, err
}
if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
if err := chartutil.ProcessDependenciesWithMerge(chrt, vals); err != nil {
return nil, err
}
var interactWithRemote bool
if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" {
interactWithRemote = true
}
// 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 {
if i.isDryRun() {
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
@@ -224,7 +262,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
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}
i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard}
mem := driver.NewMemory()
mem.SetNamespace(i.Namespace)
@@ -243,7 +281,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
}
// special case for helm template --is-upgrade
isUpgrade := i.IsUpgrade && i.DryRun
isUpgrade := i.IsUpgrade && i.isDryRun()
options := chartutil.ReleaseOptions{
Name: i.ReleaseName,
Namespace: i.Namespace,
@@ -256,10 +294,14 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
return nil, err
}
rel := i.createRelease(chrt, vals)
if driver.ContainsSystemLabels(i.Labels) {
return nil, fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels())
}
rel := i.createRelease(chrt, vals, i.Labels)
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, i.EnableDNS)
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS)
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()
@@ -295,12 +337,12 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
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")
return nil, errors.Wrap(err, "Unable to continue with install")
}
}
// Bail out here if it is a dry run
if i.DryRun {
if i.isDryRun() {
rel.Info.Description = "Dry run complete"
return rel, nil
}
@@ -346,23 +388,48 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
// 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
rel, err = i.performInstallCtx(ctx, rel, toBeAdopted, resources)
if err != nil {
rel, err = i.failRelease(rel, err)
}
return rel, err
}
func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) {
func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
type Msg struct {
r *release.Release
e error
}
resultChan := make(chan Msg, 1)
go func() {
rel, err := i.performInstall(rel, toBeAdopted, resources)
resultChan <- Msg{rel, err}
}()
select {
case <-ctx.Done():
err := ctx.Err()
return rel, err
case msg := <-resultChan:
return msg.r, msg.e
}
}
// isDryRun returns true if Upgrade is set to run as a DryRun
func (i *Install) isDryRun() bool {
if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" {
return true
}
return false
}
func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
var err error
// 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
return rel, fmt.Errorf("failed pre-install: %s", err)
}
}
@@ -370,35 +437,28 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t
// 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
}
_, err = i.cfg.KubeClient.Create(resources)
} else if len(resources) > 0 {
if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force); err != nil {
i.reportToRun(c, rel, err)
return
}
_, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force)
}
if err != nil {
return rel, err
}
if i.Wait {
if i.WaitForJobs {
if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil {
i.reportToRun(c, rel, err)
return
}
err = i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout)
} else {
if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil {
i.reportToRun(c, rel, err)
return
}
err = i.cfg.KubeClient.Wait(resources, i.Timeout)
}
if err != nil {
return rel, err
}
}
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
return rel, fmt.Errorf("failed post-install: %s", err)
}
}
@@ -419,25 +479,9 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t
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()
return rel, nil
}
func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error()))
if i.Atomic {
@@ -469,7 +513,8 @@ func (i *Install) availableName() error {
if err := chartutil.ValidateReleaseName(start); err != nil {
return errors.Wrapf(err, "release name %q", start)
}
if i.DryRun {
// On dry run, bail here
if i.isDryRun() {
return nil
}
@@ -487,7 +532,7 @@ func (i *Install) availableName() error {
}
// createRelease creates a new release object
func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release {
func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release {
ts := i.cfg.Now()
return &release.Release{
Name: i.ReleaseName,
@@ -500,6 +545,7 @@ func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{
Status: release.StatusUnknown,
},
Version: 1,
Labels: labels,
}
}
@@ -676,8 +722,6 @@ OUTER:
//
// 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)
}
@@ -709,6 +753,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
getter.WithPassCredentialsAll(c.PassCredentialsAll),
getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify),
getter.WithPlainHTTP(c.PlainHTTP),
},
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,

View File

@@ -17,7 +17,6 @@ limitations under the License.
package action
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -37,6 +36,7 @@ type Lint struct {
Namespace string
WithSubcharts bool
Quiet bool
KubeVersion *chartutil.KubeVersion
}
// LintResult is the result of Lint
@@ -59,7 +59,7 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
}
result := &LintResult{}
for _, path := range paths {
linter, err := lintChart(path, vals, l.Namespace, l.Strict)
linter, err := lintChart(path, vals, l.Namespace, l.KubeVersion)
if err != nil {
result.Errors = append(result.Errors, err)
continue
@@ -83,15 +83,15 @@ func HasWarningsOrErrors(result *LintResult) bool {
return true
}
}
return false
return len(result.Errors) > 0
}
func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) {
func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) (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")
tempDir, err := os.MkdirTemp("", "helm-lint")
if err != nil {
return linter, errors.Wrap(err, "unable to create temp dir to extract tarball")
}
@@ -125,5 +125,5 @@ func lintChart(path string, vals map[string]interface{}, namespace string, stric
return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart")
}
return lint.All(chartPath, vals, namespace, strict), nil
return lint.AllWithKubeVersion(chartPath, vals, namespace, kubeVersion), nil
}

View File

@@ -19,7 +19,6 @@ package action
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"syscall"
@@ -55,7 +54,7 @@ func NewPackage() *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) {
func (p *Package) Run(path string, _ map[string]interface{}) (string, error) {
ch, err := loader.LoadDir(path)
if err != nil {
return "", err
@@ -137,7 +136,7 @@ func (p *Package) Clearsign(filename string) error {
return err
}
return ioutil.WriteFile(filename+".prov", []byte(sig), 0644)
return os.WriteFile(filename+".prov", []byte(sig), 0644)
}
// promptUser implements provenance.PassphraseFetcher

View File

@@ -18,7 +18,6 @@ package action
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -72,6 +71,11 @@ func NewPullWithOpts(opts ...PullOpt) *Pull {
return p
}
// SetRegistryClient sets the registry client on the pull configuration object.
func (p *Pull) SetRegistryClient(client *registry.Client) {
p.cfg.RegistryClient = client
}
// Run executes 'helm pull' against the given release.
func (p *Pull) Run(chartRef string) (string, error) {
var out strings.Builder
@@ -86,6 +90,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
getter.WithPassCredentialsAll(p.PassCredentialsAll),
getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile),
getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify),
getter.WithPlainHTTP(p.PlainHTTP),
},
RegistryClient: p.cfg.RegistryClient,
RepositoryConfig: p.Settings.RepositoryConfig,
@@ -95,6 +100,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
if registry.IsOCI(chartRef) {
c.Options = append(c.Options,
getter.WithRegistryClient(p.cfg.RegistryClient))
c.RegistryClient = p.cfg.RegistryClient
}
if p.Verify {
@@ -108,7 +114,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
dest := p.DestDir
if p.Untar {
var err error
dest, err = ioutil.TempDir("", "helm-")
dest, err = os.MkdirTemp("", "helm-")
if err != nil {
return out.String(), errors.Wrap(err, "failed to untar")
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
package action
import (
"io"
"strings"
"helm.sh/helm/v3/pkg/cli"
@@ -29,8 +30,14 @@ import (
//
// It provides the implementation of 'helm push'.
type Push struct {
Settings *cli.EnvSettings
cfg *Configuration
Settings *cli.EnvSettings
cfg *Configuration
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
plainHTTP bool
out io.Writer
}
// PushOpt is a type of function that sets options for a push action.
@@ -43,6 +50,36 @@ func WithPushConfig(cfg *Configuration) PushOpt {
}
}
// WithTLSClientConfig sets the certFile, keyFile, and caFile fields on the push configuration object.
func WithTLSClientConfig(certFile, keyFile, caFile string) PushOpt {
return func(p *Push) {
p.certFile = certFile
p.keyFile = keyFile
p.caFile = caFile
}
}
// WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked
func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) PushOpt {
return func(p *Push) {
p.insecureSkipTLSverify = insecureSkipTLSVerify
}
}
// WithPlainHTTP configures the use of plain HTTP connections.
func WithPlainHTTP(plainHTTP bool) PushOpt {
return func(p *Push) {
p.plainHTTP = plainHTTP
}
}
// WithOptWriter sets the registryOut field on the push configuration object.
func WithPushOptWriter(out io.Writer) PushOpt {
return func(p *Push) {
p.out = out
}
}
// NewPushWithOpts creates a new push, with configuration options.
func NewPushWithOpts(opts ...PushOpt) *Push {
p := &Push{}
@@ -59,10 +96,15 @@ func (p *Push) Run(chartRef string, remote string) (string, error) {
c := uploader.ChartUploader{
Out: &out,
Pushers: pusher.All(p.Settings),
Options: []pusher.Option{},
Options: []pusher.Option{
pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSverify),
pusher.WithPlainHTTP(p.plainHTTP),
},
}
if registry.IsOCI(remote) {
// Don't use the default registry client if tls options are set.
c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient))
}

View File

@@ -24,7 +24,45 @@ import (
// RegistryLogin performs a registry login operation.
type RegistryLogin struct {
cfg *Configuration
cfg *Configuration
certFile string
keyFile string
caFile string
insecure bool
}
type RegistryLoginOpt func(*RegistryLogin) error
// WithCertFile specifies the path to the certificate file to use for TLS.
func WithCertFile(certFile string) RegistryLoginOpt {
return func(r *RegistryLogin) error {
r.certFile = certFile
return nil
}
}
// WithKeyFile specifies whether to very certificates when communicating.
func WithInsecure(insecure bool) RegistryLoginOpt {
return func(r *RegistryLogin) error {
r.insecure = insecure
return nil
}
}
// WithKeyFile specifies the path to the key file to use for TLS.
func WithKeyFile(keyFile string) RegistryLoginOpt {
return func(r *RegistryLogin) error {
r.keyFile = keyFile
return nil
}
}
// WithCAFile specifies the path to the CA file to use for TLS.
func WithCAFile(caFile string) RegistryLoginOpt {
return func(r *RegistryLogin) error {
r.caFile = caFile
return nil
}
}
// NewRegistryLogin creates a new RegistryLogin object with the given configuration.
@@ -35,9 +73,16 @@ func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
}
// Run executes the registry login operation
func (a *RegistryLogin) Run(out io.Writer, hostname string, username string, password string, insecure bool) error {
func (a *RegistryLogin) Run(_ io.Writer, hostname string, username string, password string, opts ...RegistryLoginOpt) error {
for _, opt := range opts {
if err := opt(a); err != nil {
return err
}
}
return a.cfg.RegistryClient.Login(
hostname,
registry.LoginOptBasicAuth(username, password),
registry.LoginOptInsecure(insecure))
registry.LoginOptInsecure(a.insecure),
registry.LoginOptTLSClientConfig(a.certFile, a.keyFile, a.caFile))
}

View File

@@ -33,6 +33,6 @@ func NewRegistryLogout(cfg *Configuration) *RegistryLogout {
}
// Run executes the registry logout operation
func (a *RegistryLogout) Run(out io.Writer, hostname string) error {
func (a *RegistryLogout) Run(_ io.Writer, hostname string) error {
return a.cfg.RegistryClient.Logout(hostname)
}

View File

@@ -20,6 +20,7 @@ import (
"context"
"fmt"
"io"
"sort"
"time"
"github.com/pkg/errors"
@@ -29,6 +30,11 @@ import (
"helm.sh/helm/v3/pkg/release"
)
const (
ExcludeNameFilter = "!name"
IncludeNameFilter = "name"
)
// ReleaseTesting is the action for testing a release.
//
// It provides the implementation of 'helm test'.
@@ -66,9 +72,9 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
skippedHooks := []*release.Hook{}
executingHooks := []*release.Hook{}
if len(r.Filters["!name"]) != 0 {
if len(r.Filters[ExcludeNameFilter]) != 0 {
for _, h := range rel.Hooks {
if contains(r.Filters["!name"], h.Name) {
if contains(r.Filters[ExcludeNameFilter], h.Name) {
skippedHooks = append(skippedHooks, h)
} else {
executingHooks = append(executingHooks, h)
@@ -76,10 +82,10 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
}
rel.Hooks = executingHooks
}
if len(r.Filters["name"]) != 0 {
if len(r.Filters[IncludeNameFilter]) != 0 {
executingHooks = nil
for _, h := range rel.Hooks {
if contains(r.Filters["name"], h.Name) {
if contains(r.Filters[IncludeNameFilter], h.Name) {
executingHooks = append(executingHooks, h)
} else {
skippedHooks = append(skippedHooks, h)
@@ -107,9 +113,17 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error {
return errors.Wrap(err, "unable to get kubernetes client to fetch pod logs")
}
for _, h := range rel.Hooks {
hooksByWight := append([]*release.Hook{}, rel.Hooks...)
sort.Stable(hookByWeight(hooksByWight))
for _, h := range hooksByWight {
for _, e := range h.Events {
if e == release.HookTest {
if contains(r.Filters[ExcludeNameFilter], h.Name) {
continue
}
if len(r.Filters[IncludeNameFilter]) > 0 && !contains(r.Filters[IncludeNameFilter], h.Name) {
continue
}
req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{})
logReader, err := req.Stream(context.Background())
if err != nil {

View File

@@ -110,6 +110,24 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
previousVersion = currentRelease.Version - 1
}
historyReleases, err := r.cfg.Releases.History(name)
if err != nil {
return nil, nil, err
}
// Check if the history version to be rolled back exists
previousVersionExist := false
for _, historyRelease := range historyReleases {
version := historyRelease.Version
if previousVersion == version {
previousVersionExist = true
break
}
}
if !previousVersionExist {
return nil, nil, errors.Errorf("release has no %d version", previousVersion)
}
r.cfg.Log("rolling back %s (current: v%d, target: v%d)", name, currentRelease.Version, previousVersion)
previousRelease, err := r.cfg.Releases.Get(name, previousVersion)
@@ -133,6 +151,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
Description: fmt.Sprintf("Rollback to %d", previousVersion),
},
Version: currentRelease.Version + 1,
Labels: previousRelease.Labels,
Manifest: previousRelease.Manifest,
Hooks: previousRelease.Hooks,
}

View File

@@ -28,6 +28,7 @@ import (
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/registry"
)
// ShowOutputFormat is the format of the output of `helm show`
@@ -82,6 +83,11 @@ func NewShowWithConfig(output ShowOutputFormat, cfg *Configuration) *Show {
return sh
}
// SetRegistryClient sets the registry client to use when pulling a chart from a registry.
func (s *Show) SetRegistryClient(client *registry.Client) {
s.ChartPathOptions.registryClient = client
}
// Run executes 'helm show' against the given release.
func (s *Show) Run(chartpath string) (string, error) {
if s.chart == nil {
@@ -147,6 +153,9 @@ func (s *Show) Run(chartpath string) (string, error) {
func findReadme(files []*chart.File) (file *chart.File) {
for _, file := range files {
for _, n := range readmeFileNames {
if file == nil {
continue
}
if strings.EqualFold(file.Name, n) {
return file
}

View File

@@ -22,6 +22,8 @@ import (
"github.com/pkg/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release"
@@ -35,12 +37,14 @@ import (
type Uninstall struct {
cfg *Configuration
DisableHooks bool
DryRun bool
KeepHistory bool
Wait bool
Timeout time.Duration
Description string
DisableHooks bool
DryRun bool
IgnoreNotFound bool
KeepHistory bool
Wait bool
DeletionPropagation string
Timeout time.Duration
Description string
}
// NewUninstall creates a new Uninstall object with the given configuration.
@@ -71,6 +75,9 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
rels, err := u.cfg.Releases.History(name)
if err != nil {
if u.IgnoreNotFound {
return nil, nil
}
return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", name)
}
if len(rels) < 1 {
@@ -220,7 +227,25 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri
return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")}
}
if len(resources) > 0 {
if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok {
_, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.cfg, u.DeletionPropagation))
return resources, kept, errs
}
_, errs = u.cfg.KubeClient.Delete(resources)
}
return resources, kept, errs
}
func parseCascadingFlag(cfg *Configuration, cascadingFlag string) v1.DeletionPropagation {
switch cascadingFlag {
case "orphan":
return v1.DeletePropagationOrphan
case "foreground":
return v1.DeletePropagationForeground
case "background":
return v1.DeletePropagationBackground
default:
cfg.Log("uninstall: given cascade value: %s, defaulting to delete propagation background", cascadingFlag)
return v1.DeletePropagationBackground
}
}

View File

@@ -32,6 +32,7 @@ import (
"helm.sh/helm/v3/pkg/chartutil"
"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/driver"
@@ -70,8 +71,9 @@ type Upgrade struct {
// 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
// DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster.
DryRunOption string
// Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway.
//
// This should be used with caution.
@@ -80,6 +82,8 @@ type Upgrade struct {
ResetValues bool
// ReuseValues will re-use the user's last supplied values.
ReuseValues bool
// ResetThenReuseValues will reset the values to the chart's built-ins then merge with user's last supplied values.
ResetThenReuseValues bool
// Recreate will (if true) recreate pods after a rollback.
Recreate bool
// MaxHistory limits the maximum number of revisions saved per release
@@ -92,6 +96,7 @@ type Upgrade struct {
SubNotes bool
// Description is the description of this operation
Description string
Labels map[string]string
// PostRender is an optional post-renderer
//
// If this is non-nil, then after templates are rendered, they will be sent to the
@@ -122,6 +127,11 @@ func NewUpgrade(cfg *Configuration) *Upgrade {
return up
}
// SetRegistryClient sets the registry client to use when fetching charts.
func (u *Upgrade) SetRegistryClient(client *registry.Client) {
u.ChartPathOptions.registryClient = client
}
// 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()
@@ -141,6 +151,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
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 {
@@ -155,7 +166,8 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
return res, err
}
if !u.DryRun {
// Do not update for dry runs
if !u.isDryRun() {
u.cfg.Log("updating status for upgraded release for %s", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err
@@ -165,6 +177,14 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
return res, nil
}
// isDryRun returns true if Upgrade is set to run as a DryRun
func (u *Upgrade) isDryRun() bool {
if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" {
return true
}
return false
}
// 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 {
@@ -209,7 +229,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err
}
if err := chartutil.ProcessDependencies(chart, vals); err != nil {
if err := chartutil.ProcessDependenciesWithMerge(chart, vals); err != nil {
return nil, nil, err
}
@@ -233,11 +253,21 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun, u.EnableDNS)
// Determine whether or not to interact with remote
var interactWithRemote bool
if !u.isDryRun() || u.DryRunOption == "server" || u.DryRunOption == "none" || u.DryRunOption == "false" {
interactWithRemote = true
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS)
if err != nil {
return nil, nil, err
}
if driver.ContainsSystemLabels(u.Labels) {
return nil, nil, fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels())
}
// Store an upgraded release.
upgradedRelease := &release.Release{
Name: name,
@@ -253,6 +283,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
Version: revision,
Manifest: manifestDoc.String(),
Hooks: hooks,
Labels: mergeCustomLabels(lastRelease.Labels, u.Labels),
}
if len(notesTxt) > 0 {
@@ -300,7 +331,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
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")
return nil, errors.Wrap(err, "Unable to continue with update")
}
toBeUpdated.Visit(func(r *resource.Info, err error) error {
@@ -311,7 +342,8 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
return nil
})
if u.DryRun {
// Run if it is a dry run
if u.isDryRun() {
u.cfg.Log("dry run for %s", upgradedRelease.Name)
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description
@@ -522,6 +554,15 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV
return newVals, nil
}
// If the ResetThenReuseValues flag is set, we use the new chart's values, but we copy the old config's values over the new config's values.
if u.ResetThenReuseValues {
u.cfg.Log("merging values from old release to new values")
newVals = chartutil.CoalesceTables(newVals, current.Config)
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
@@ -574,3 +615,13 @@ 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)
}
func mergeCustomLabels(current, desired map[string]string) map[string]string {
labels := mergeStrStrMaps(current, desired)
for k, v := range labels {
if v == "null" {
delete(labels, k)
}
}
return labels
}

View File

@@ -85,7 +85,10 @@ func ensureArchive(name string, raw *os.File) error {
if err != nil && err != io.EOF {
return fmt.Errorf("file '%s' cannot be read: %s", name, err)
}
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" {
// Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject.
// Fix for: https://github.com/helm/helm/issues/12261
if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) {
// TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide
// variety of content (Makefile, .zshrc) as valid YAML without errors.
@@ -98,6 +101,12 @@ func ensureArchive(name string, raw *os.File) error {
return nil
}
// isGZipApplication checks whether the achieve is of the application/x-gzip type.
func isGZipApplication(data []byte) bool {
sig := []byte("\x1F\x8B\x08")
return bytes.HasPrefix(data, sig)
}
// LoadArchiveFiles reads in files out of an archive into memory. This function
// performs important path security checks and should always be used before
// expanding a tarball

View File

@@ -19,16 +19,15 @@ package loader
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/ignore"
"helm.sh/helm/v3/internal/sympath"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/ignore"
)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}
@@ -102,7 +101,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
}
data, err := ioutil.ReadFile(name)
data, err := os.ReadFile(name)
if err != nil {
return errors.Wrapf(err, "error reading %s", n)
}

View File

@@ -16,6 +16,7 @@ limitations under the License.
package chart
import (
"path/filepath"
"strings"
"unicode"
@@ -110,6 +111,11 @@ func (md *Metadata) Validate() error {
if md.Name == "" {
return ValidationError("chart.metadata.name is required")
}
if md.Name != filepath.Base(md.Name) {
return ValidationErrorf("chart.metadata.name %q is invalid", md.Name)
}
if md.Version == "" {
return ValidationError("chart.metadata.version is required")
}
@@ -128,10 +134,19 @@ func (md *Metadata) Validate() error {
// Aliases need to be validated here to make sure that the alias name does
// not contain any illegal characters.
dependencies := map[string]*Dependency{}
for _, dependency := range md.Dependencies {
if err := dependency.Validate(); err != nil {
return err
}
key := dependency.Name
if dependency.Alias != "" {
key = dependency.Alias
}
if dependencies[key] != nil {
return ValidationErrorf("more than one dependency with name or alias %q", key)
}
dependencies[key] = dependency
}
return nil
}

View File

@@ -17,7 +17,6 @@ limitations under the License.
package chartutil
import (
"io/ioutil"
"os"
"path/filepath"
@@ -29,7 +28,7 @@ import (
// LoadChartfile loads a Chart.yaml file into a *chart.Metadata.
func LoadChartfile(filename string) (*chart.Metadata, error) {
b, err := ioutil.ReadFile(filename)
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
@@ -55,7 +54,7 @@ func SaveChartfile(filename string, cf *chart.Metadata) error {
if err != nil {
return err
}
return ioutil.WriteFile(filename, out, 0644)
return os.WriteFile(filename, out, 0644)
}
// IsChartDir validate a chart directory.
@@ -73,7 +72,7 @@ func IsChartDir(dirName string) (bool, error) {
return false, errors.Errorf("no %s exists in directory %q", ChartfileName, dirName)
}
chartYamlContent, err := ioutil.ReadFile(chartYaml)
chartYamlContent, err := os.ReadFile(chartYaml)
if err != nil {
return false, errors.Errorf("cannot read %s in directory %q", ChartfileName, dirName)
}

View File

@@ -37,12 +37,42 @@ func concatPrefix(a, b string) string {
//
// Values are coalesced together using the following rules:
//
// - Values in a higher level chart always override values in a lower-level
// dependency chart
// - Scalar values and arrays are replaced, maps are merged
// - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies.
// - Values in a higher level chart always override values in a lower-level
// dependency chart
// - Scalar values and arrays are replaced, maps are merged
// - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies.
func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
valsCopy, err := copyValues(vals)
if err != nil {
return vals, err
}
return coalesce(log.Printf, chrt, valsCopy, "", false)
}
// MergeValues is used to merge the values in a chart and its subcharts. This
// is different from Coalescing as nil/null values are preserved.
//
// Values are coalesced together using the following rules:
//
// - Values in a higher level chart always override values in a lower-level
// dependency chart
// - Scalar values and arrays are replaced, maps are merged
// - A chart has access to all of the variables for it, as well as all of
// the values destined for its dependencies.
//
// Retaining Nils is useful when processes early in a Helm action or business
// logic need to retain them for when Coalescing will happen again later in the
// business logic.
func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) {
valsCopy, err := copyValues(vals)
if err != nil {
return vals, err
}
return coalesce(log.Printf, chrt, valsCopy, "", true)
}
func copyValues(vals map[string]interface{}) (Values, error) {
v, err := copystructure.Copy(vals)
if err != nil {
return vals, err
@@ -53,21 +83,26 @@ func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, err
if valsCopy == nil {
valsCopy = make(map[string]interface{})
}
return coalesce(log.Printf, chrt, valsCopy, "")
return valsCopy, nil
}
type printFn func(format string, v ...interface{})
// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
//
// This is a helper function for CoalesceValues.
func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string) (map[string]interface{}, error) {
coalesceValues(printf, ch, dest, prefix)
return coalesceDeps(printf, ch, dest, prefix)
// This is a helper function for CoalesceValues and MergeValues.
//
// Note, the merge argument specifies whether this is being used by MergeValues
// or CoalesceValues. Coalescing removes null values and their keys in some
// situations while merging keeps the null values.
func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) {
coalesceValues(printf, ch, dest, prefix, merge)
return coalesceDeps(printf, ch, dest, prefix, merge)
}
// coalesceDeps coalesces the dependencies of the given chart.
func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string) (map[string]interface{}, error) {
func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) {
for _, subchart := range chrt.Dependencies() {
if c, ok := dest[subchart.Name()]; !ok {
// If dest doesn't already have the key, create it.
@@ -78,13 +113,11 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}
if dv, ok := dest[subchart.Name()]; ok {
dvmap := dv.(map[string]interface{})
subPrefix := concatPrefix(prefix, chrt.Metadata.Name)
// Get globals out of dest and merge them into dvmap.
coalesceGlobals(printf, dvmap, dest, subPrefix)
coalesceGlobals(printf, dvmap, dest, subPrefix, merge)
// Now coalesce the rest of the values.
var err error
dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix)
dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge)
if err != nil {
return dest, err
}
@@ -96,7 +129,7 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}
// coalesceGlobals copies the globals out of src and merges them into dest.
//
// For convenience, returns dest.
func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string) {
func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) {
var dg, sg map[string]interface{}
if destglob, ok := dest[GlobalKey]; !ok {
@@ -130,7 +163,10 @@ func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix st
// Basically, we reverse order of coalesce here to merge
// top-down.
subPrefix := concatPrefix(prefix, key)
coalesceTablesFullKey(printf, vv, destvmap, subPrefix)
// In this location coalesceTablesFullKey should always have
// merge set to true. The output of coalesceGlobals is run
// through coalesce where any nils will be removed.
coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true)
dg[key] = vv
}
}
@@ -156,12 +192,38 @@ func copyMap(src map[string]interface{}) map[string]interface{} {
// coalesceValues builds up a values map for a particular chart.
//
// Values in v will override the values in the chart.
func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string) {
func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) {
subPrefix := concatPrefix(prefix, c.Metadata.Name)
for key, val := range c.Values {
// Using c.Values directly when coalescing a table can cause problems where
// the original c.Values is altered. Creating a deep copy stops the problem.
// This section is fault-tolerant as there is no ability to return an error.
valuesCopy, err := copystructure.Copy(c.Values)
var vc map[string]interface{}
var ok bool
if err != nil {
// If there is an error something is wrong with copying c.Values it
// means there is a problem in the deep copying package or something
// wrong with c.Values. In this case we will use c.Values and report
// an error.
printf("warning: unable to copy values, err: %s", err)
vc = c.Values
} else {
vc, ok = valuesCopy.(map[string]interface{})
if !ok {
// c.Values has a map[string]interface{} structure. If the copy of
// it cannot be treated as map[string]interface{} there is something
// strangely wrong. Log it and use c.Values
printf("warning: unable to convert values copy to values type")
vc = c.Values
}
}
for key, val := range vc {
if value, ok := v[key]; ok {
if value == nil {
// When the YAML value is null, we remove the value's key.
if value == nil && !merge {
// When the YAML value is null and we are coalescing instead of
// merging, we remove the value's key.
// This allows Helm's various sources of values (value files or --set) to
// remove incompatible keys from any previous chart, file, or set values.
delete(v, key)
@@ -177,7 +239,7 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr
} else {
// Because v has higher precedence than nv, dest values override src
// values.
coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key))
coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge)
}
}
} else {
@@ -191,13 +253,17 @@ func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, pr
//
// dest is considered authoritative.
func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} {
return coalesceTablesFullKey(log.Printf, dst, src, "")
return coalesceTablesFullKey(log.Printf, dst, src, "", false)
}
func MergeTables(dst, src map[string]interface{}) map[string]interface{} {
return coalesceTablesFullKey(log.Printf, dst, src, "", true)
}
// coalesceTablesFullKey merges a source map into a destination map.
//
// dest is considered authoritative.
func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string) map[string]interface{} {
func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} {
// When --reuse-values is set but there are no modifications yet, return new values
if src == nil {
return dst
@@ -209,13 +275,13 @@ func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, pref
// values.
for key, val := range src {
fullkey := concatPrefix(prefix, key)
if dv, ok := dst[key]; ok && dv == nil {
if dv, ok := dst[key]; ok && !merge && dv == nil {
delete(dst, key)
} else if !ok {
dst[key] = val
} else if istable(val) {
if istable(dv) {
coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey)
coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge)
} else {
printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val)
}

View File

@@ -19,7 +19,6 @@ package chartutil
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@@ -122,6 +121,8 @@ fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
@@ -129,6 +130,7 @@ serviceAccount:
name: ""
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
# fsGroup: 2000
@@ -173,6 +175,15 @@ resources: {}
# cpu: 100m
# memory: 128Mi
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
autoscaling:
enabled: false
minReplicas: 1
@@ -180,6 +191,19 @@ autoscaling:
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}
tolerations: []
@@ -295,7 +319,10 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "<CHARTNAME>.selectorLabels" . | nindent 8 }}
{{- include "<CHARTNAME>.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
@@ -315,15 +342,19 @@ spec:
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
httpGet:
path: /
port: http
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
@@ -366,11 +397,12 @@ metadata:
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}
`
const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "<CHARTNAME>.fullname" . }}
@@ -388,13 +420,17 @@ spec:
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
`
@@ -673,7 +709,7 @@ func writeFile(name string, content []byte) error {
if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil {
return err
}
return ioutil.WriteFile(name, content, 0644)
return os.WriteFile(name, content, 0644)
}
func validateChartName(name string) error {

View File

@@ -19,15 +19,29 @@ import (
"log"
"strings"
"github.com/mitchellh/copystructure"
"helm.sh/helm/v3/pkg/chart"
)
// ProcessDependencies checks through this chart's dependencies, processing accordingly.
//
// TODO: For Helm v4 this can be combined with or turned into ProcessDependenciesWithMerge
func ProcessDependencies(c *chart.Chart, v Values) error {
if err := processDependencyEnabled(c, v, ""); err != nil {
return err
}
return processDependencyImportValues(c)
return processDependencyImportValues(c, false)
}
// ProcessDependenciesWithMerge checks through this chart's dependencies, processing accordingly.
// It is similar to ProcessDependencies but it does not remove nil values during
// the import/export handling process.
func ProcessDependenciesWithMerge(c *chart.Chart, v Values) error {
if err := processDependencyEnabled(c, v, ""); err != nil {
return err
}
return processDependencyImportValues(c, true)
}
// processDependencyConditions disables charts based on condition path value in values
@@ -45,9 +59,8 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s
if bv, ok := vv.(bool); ok {
r.Enabled = bv
break
} else {
log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name)
}
log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name)
} else if _, ok := err.(ErrNoValue); !ok {
// this is a real error
log.Printf("Warning: PathValue returned error %v", err)
@@ -137,6 +150,9 @@ Loop:
}
for _, req := range c.Metadata.Dependencies {
if req == nil {
continue
}
if chartDependency := getAliasDependency(c.Dependencies(), req); chartDependency != nil {
chartDependencies = append(chartDependencies, chartDependency)
}
@@ -217,12 +233,18 @@ func set(path []string, data map[string]interface{}) map[string]interface{} {
}
// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
func processImportValues(c *chart.Chart) error {
func processImportValues(c *chart.Chart, merge bool) error {
if c.Metadata.Dependencies == nil {
return nil
}
// combine chart values and empty config to get Values
cvals, err := CoalesceValues(c, nil)
var cvals Values
var err error
if merge {
cvals, err = MergeValues(c, nil)
} else {
cvals, err = CoalesceValues(c, nil)
}
if err != nil {
return err
}
@@ -248,7 +270,11 @@ func processImportValues(c *chart.Chart) error {
continue
}
// create value map from child to be merged into parent
b = CoalesceTables(cvals, pathToMap(parent, vv.AsMap()))
if merge {
b = MergeTables(b, pathToMap(parent, vv.AsMap()))
} else {
b = CoalesceTables(b, pathToMap(parent, vv.AsMap()))
}
case string:
child := "exports." + iv
outiv = append(outiv, map[string]string{
@@ -260,26 +286,71 @@ func processImportValues(c *chart.Chart) error {
log.Printf("Warning: ImportValues missing table: %v", err)
continue
}
b = CoalesceTables(b, vm.AsMap())
if merge {
b = MergeTables(b, vm.AsMap())
} else {
b = CoalesceTables(b, vm.AsMap())
}
}
}
// set our formatted import values
r.ImportValues = outiv
}
// set the new values
c.Values = CoalesceTables(cvals, b)
// Imported values from a child to a parent chart have a lower priority than
// the parents values. This enables parent charts to import a large section
// from a child and then override select parts. This is why b is merged into
// cvals in the code below and not the other way around.
if merge {
// deep copying the cvals as there are cases where pointers can end
// up in the cvals when they are copied onto b in ways that break things.
cvals = deepCopyMap(cvals)
c.Values = MergeTables(cvals, b)
} else {
// Trimming the nil values from cvals is needed for backwards compatibility.
// Previously, the b value had been populated with cvals along with some
// overrides. This caused the coalescing functionality to remove the
// nil/null values. This trimming is for backwards compat.
cvals = trimNilValues(cvals)
c.Values = CoalesceTables(cvals, b)
}
return nil
}
func deepCopyMap(vals map[string]interface{}) map[string]interface{} {
valsCopy, err := copystructure.Copy(vals)
if err != nil {
return vals
}
return valsCopy.(map[string]interface{})
}
func trimNilValues(vals map[string]interface{}) map[string]interface{} {
valsCopy, err := copystructure.Copy(vals)
if err != nil {
return vals
}
valsCopyMap := valsCopy.(map[string]interface{})
for key, val := range valsCopyMap {
if val == nil {
// Iterate over the values and remove nil keys
delete(valsCopyMap, key)
} else if istable(val) {
// Recursively call into ourselves to remove keys from inner tables
valsCopyMap[key] = trimNilValues(val.(map[string]interface{}))
}
}
return valsCopyMap
}
// processDependencyImportValues imports specified chart values from child to parent.
func processDependencyImportValues(c *chart.Chart) error {
func processDependencyImportValues(c *chart.Chart, merge bool) error {
for _, d := range c.Dependencies() {
// recurse
if err := processDependencyImportValues(d); err != nil {
if err := processDependencyImportValues(d, merge); err != nil {
return err
}
}
return processImportValues(c)
return processImportValues(c, merge)
}

View File

@@ -14,16 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package chartutil contains tools for working with charts.
/*
Package chartutil contains tools for working with charts.
Charts are described in the chart package (pkg/chart).
This package provides utilities for serializing and deserializing charts.
A chart can be represented on the file system in one of two ways:
- As a directory that contains a Chart.yaml file and other chart things.
- As a tarred gzipped file containing a directory that then contains a
Chart.yaml file.
- As a directory that contains a Chart.yaml file and other chart things.
- As a tarred gzipped file containing a directory that then contains a
Chart.yaml file.
This package provides utilities for working with those file formats.

View File

@@ -33,3 +33,11 @@ type ErrNoValue struct {
}
func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) }
type ErrInvalidChartName struct {
Name string
}
func (e ErrInvalidChartName) Error() string {
return fmt.Sprintf("%q is not a valid chart name", e.Name)
}

View File

@@ -18,7 +18,6 @@ package chartutil
import (
"io"
"io/ioutil"
"os"
"path/filepath"
@@ -72,7 +71,7 @@ func Expand(dir string, r io.Reader) error {
return err
}
if err := ioutil.WriteFile(outpath, file.Data, 0644); err != nil {
if err := os.WriteFile(outpath, file.Data, 0644); err != nil {
return err
}
}

View File

@@ -39,6 +39,10 @@ var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=")
// directory, writing the chart's contents to that subdirectory.
func SaveDir(c *chart.Chart, dest string) error {
// Create the chart directory
err := validateName(c.Name())
if err != nil {
return err
}
outdir := filepath.Join(dest, c.Name())
if fi, err := os.Stat(outdir); err == nil && !fi.IsDir() {
return errors.Errorf("file %s already exists and is not a directory", outdir)
@@ -149,6 +153,10 @@ func Save(c *chart.Chart, outDir string) (string, error) {
}
func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
err := validateName(c.Name())
if err != nil {
return err
}
base := filepath.Join(prefix, c.Name())
// Pull out the dependencies of a v1 Chart, since there's no way
@@ -242,3 +250,15 @@ func writeToTar(out *tar.Writer, name string, body []byte) error {
_, err := out.Write(body)
return err
}
// If the name has directory name has characters which would change the location
// they need to be removed.
func validateName(name string) error {
nname := filepath.Base(name)
if nname != name {
return ErrInvalidChartName{name}
}
return nil
}

View File

@@ -19,7 +19,7 @@ package chartutil
import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/pkg/errors"
@@ -114,7 +114,7 @@ func ReadValues(data []byte) (vals Values, err error) {
// ReadValuesFile will parse a YAML file into a map of values.
func ReadValuesFile(filename string) (Values, error) {
data, err := ioutil.ReadFile(filename)
data, err := os.ReadFile(filename)
if err != nil {
return map[string]interface{}{}, err
}

View File

@@ -44,6 +44,9 @@ const defaultMaxHistory = 10
// defaultBurstLimit sets the default client-side throttling limit
const defaultBurstLimit = 100
// defaultQPS sets the default QPS value to 0 to to use library defaults unless specified
const defaultQPS = float32(0)
// EnvSettings describes all of the environment settings.
type EnvSettings struct {
namespace string
@@ -83,6 +86,8 @@ type EnvSettings struct {
MaxHistory int
// BurstLimit is the default client-side throttling limit.
BurstLimit int
// QPS is queries per second which may be used to avoid throttling.
QPS float32
}
func New() *EnvSettings {
@@ -102,6 +107,7 @@ func New() *EnvSettings {
RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")),
RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")),
BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit),
QPS: envFloat32Or("HELM_QPS", defaultQPS),
}
env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG"))
@@ -119,6 +125,7 @@ func New() *EnvSettings {
ImpersonateGroup: &env.KubeAsGroups,
WrapConfigFn: func(config *rest.Config) *rest.Config {
config.Burst = env.BurstLimit
config.QPS = env.QPS
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return &retryingRoundTripper{wrapped: rt}
})
@@ -146,6 +153,7 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs")
fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the file containing cached repository indexes")
fs.IntVar(&s.BurstLimit, "burst-limit", s.BurstLimit, "client-side default throttling limit")
fs.Float32Var(&s.QPS, "qps", s.QPS, "queries per second used when communicating with the Kubernetes API, not including bursting")
}
func envOr(name, def string) string {
@@ -179,6 +187,18 @@ func envIntOr(name string, def int) int {
return ret
}
func envFloat32Or(name string, def float32) float32 {
if name == "" {
return def
}
envVal := envOr(name, strconv.FormatFloat(float64(def), 'f', 2, 32))
ret, err := strconv.ParseFloat(envVal, 32)
if err != nil {
return def
}
return float32(ret)
}
func envCSV(name string) (ls []string) {
trimmed := strings.Trim(os.Getenv(name), ", ")
if trimmed != "" {
@@ -201,6 +221,7 @@ func (s *EnvSettings) EnvVars() map[string]string {
"HELM_NAMESPACE": s.Namespace(),
"HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory),
"HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit),
"HELM_QPS": strconv.FormatFloat(float64(s.QPS), 'f', 2, 32),
// broken, these are populated from helm flags and not kubeconfig.
"HELM_KUBECONTEXT": s.KubeContext,

View File

@@ -184,11 +184,11 @@ func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL,
//
// A version is a SemVer string (1.2.3-beta.1+f334a6789).
//
// - For fully qualified URLs, the version will be ignored (since URLs aren't versioned)
// - For a chart reference
// * If version is non-empty, this will return the URL for that version
// * If version is empty, this will return the URL for the latest version
// * If no version can be found, an error is returned
// - For fully qualified URLs, the version will be ignored (since URLs aren't versioned)
// - For a chart reference
// - If version is non-empty, this will return the URL for that version
// - If version is empty, this will return the URL for the latest version
// - If no version can be found, an error is returned
func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) {
u, err := url.Parse(ref)
if err != nil {

View File

@@ -13,7 +13,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package downloader provides a library for downloading charts.
/*
Package downloader provides a library for downloading charts.
This package contains various tools for downloading charts from repository
servers, and then storing them in Helm-specific directory structures. This

View File

@@ -20,7 +20,6 @@ import (
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"log"
"net/url"
"os"
@@ -249,7 +248,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
destPath := filepath.Join(m.ChartPath, "charts")
tmpPath := filepath.Join(m.ChartPath, "tmpcharts")
// Check if 'charts' directory is not actally a directory. If it does not exist, create it.
// Check if 'charts' directory is not actually a directory. If it does not exist, create it.
if fi, err := os.Stat(destPath); err == nil {
if !fi.IsDir() {
return errors.Errorf("%q is not a directory", destPath)
@@ -668,6 +667,7 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
if err != nil {
return err
}
r.CachePath = m.RepositoryCache
wg.Add(1)
go func(r *repo.ChartRepository) {
if _, err := r.DownloadIndexFile(); err != nil {
@@ -714,15 +714,21 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
var entry repo.ChartVersions
entry, err = findEntryByName(name, cr)
if err != nil {
// TODO: Where linting is skipped in this function we should
// refactor to remove naked returns while ensuring the same
// behavior
//nolint:nakedret
return
}
var ve *repo.ChartVersion
ve, err = findVersionedEntry(version, entry)
if err != nil {
//nolint:nakedret
return
}
url, err = normalizeURL(repoURL, ve.URLs[0])
if err != nil {
//nolint:nakedret
return
}
username = cr.Config.Username
@@ -732,6 +738,7 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
caFile = cr.Config.CAFile
certFile = cr.Config.CertFile
keyFile = cr.Config.KeyFile
//nolint:nakedret
return
}
}
@@ -845,7 +852,7 @@ func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error {
lockfileName = "requirements.lock"
}
dest := filepath.Join(chartpath, lockfileName)
return ioutil.WriteFile(dest, data, 0644)
return os.WriteFile(dest, data, 0644)
}
// archive a dep chart from local directory and save it into destPath

View File

@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package engine implements the Go text template engine as needed for Helm.
/*
Package engine implements the Go text template engine as needed for Helm.
When Helm renders templates it does so with additional functions and different
modes (e.g., strict, lint mode). This package handles the helm specific

View File

@@ -40,16 +40,17 @@ type Engine struct {
Strict bool
// In LintMode, some 'required' template values may be missing, so don't fail
LintMode bool
// the rest config to connect to the kubernetes api
config *rest.Config
// optional provider of clients to talk to the Kubernetes API
clientProvider *ClientProvider
// EnableDNS tells the engine to allow DNS lookups when rendering templates
EnableDNS bool
}
// New creates a new instance of Engine using the passed in rest config.
func New(config *rest.Config) Engine {
var clientProvider ClientProvider = clientProviderFromConfig{config}
return Engine{
config: config,
clientProvider: &clientProvider,
}
}
@@ -85,10 +86,21 @@ func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, erro
// RenderWithClient takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client
// functions that interact with the client.
func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) {
var clientProvider ClientProvider = clientProviderFromConfig{config}
return Engine{
config: config,
clientProvider: &clientProvider,
}.Render(chrt, values)
}
// RenderWithClientProvider takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client.
// This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed.
func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider) (map[string]string, error) {
return Engine{
clientProvider: &clientProvider,
}.Render(chrt, values)
}
@@ -112,13 +124,10 @@ func warnWrap(warn string) string {
return warnStartDelim + warn + warnEndDelim
}
// initFunMap creates the Engine's FuncMap and adds context-specific functions.
func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]renderable) {
funcMap := funcMap()
includedNames := make(map[string]int)
// Add the 'include' function here so we can close over t.
funcMap["include"] = func(name string, data interface{}) (string, error) {
// 'include' needs to be defined in the scope of a 'tpl' template as
// well as regular file-loaded templates.
func includeFun(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) {
return func(name string, data interface{}) (string, error) {
var buf strings.Builder
if v, ok := includedNames[name]; ok {
if v > recursionMaxNums {
@@ -132,33 +141,62 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render
includedNames[name]--
return buf.String(), err
}
}
// Add the 'tpl' function here
funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) {
basePath, err := vals.PathValue("Template.BasePath")
// As does 'tpl', so that nested calls to 'tpl' see the templates
// defined by their enclosing contexts.
func tplFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) {
return func(tpl string, vals interface{}) (string, error) {
t, err := parent.Clone()
if err != nil {
return "", errors.Wrapf(err, "cannot retrieve Template.Basepath from values inside tpl function: %s", tpl)
return "", errors.Wrapf(err, "cannot clone template")
}
templateName, err := vals.PathValue("Template.Name")
if err != nil {
return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl)
// Re-inject the missingkey option, see text/template issue https://github.com/golang/go/issues/43022
// We have to go by strict from our engine configuration, as the option fields are private in Template.
// TODO: Remove workaround (and the strict parameter) once we build only with golang versions with a fix.
if strict {
t.Option("missingkey=error")
} else {
t.Option("missingkey=zero")
}
templates := map[string]renderable{
templateName.(string): {
tpl: tpl,
vals: vals,
basePath: basePath.(string),
},
// Re-inject 'include' so that it can close over our clone of t;
// this lets any 'define's inside tpl be 'include'd.
t.Funcs(template.FuncMap{
"include": includeFun(t, includedNames),
"tpl": tplFun(t, includedNames, strict),
})
// We need a .New template, as template text which is just blanks
// or comments after parsing out defines just addes new named
// template definitions without changing the main template.
// https://pkg.go.dev/text/template#Template.Parse
// Use the parent's name for lack of a better way to identify the tpl
// text string. (Maybe we could use a hash appended to the name?)
t, err = t.New(parent.Name()).Parse(tpl)
if err != nil {
return "", errors.Wrapf(err, "cannot parse template %q", tpl)
}
result, err := e.renderWithReferences(templates, referenceTpls)
if err != nil {
var buf strings.Builder
if err := t.Execute(&buf, vals); err != nil {
return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl)
}
return result[templateName.(string)], nil
// See comment in renderWithReferences explaining the <no value> hack.
return strings.ReplaceAll(buf.String(), "<no value>", ""), nil
}
}
// initFunMap creates the Engine's FuncMap and adds context-specific functions.
func (e Engine) initFunMap(t *template.Template) {
funcMap := funcMap()
includedNames := make(map[string]int)
// Add the template-rendering functions here so we can close over t.
funcMap["include"] = includeFun(t, includedNames)
funcMap["tpl"] = tplFun(t, includedNames, e.Strict)
// Add the `required` function here so we can use lintMode
funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
@@ -194,8 +232,8 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render
// If we are not linting and have a cluster connection, provide a Kubernetes-backed
// implementation.
if !e.LintMode && e.config != nil {
funcMap["lookup"] = NewLookupFunction(e.config)
if !e.LintMode && e.clientProvider != nil {
funcMap["lookup"] = newLookupFunction(*e.clientProvider)
}
// When DNS lookups are not enabled override the sprig function and return
@@ -210,13 +248,7 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render
}
// render takes a map of templates/values and renders them.
func (e Engine) render(tpls map[string]renderable) (map[string]string, error) {
return e.renderWithReferences(tpls, tpls)
}
// renderWithReferences takes a map of templates/values to render, and a map of
// templates which can be referenced within them.
func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) (rendered map[string]string, err error) {
func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) {
// Basically, what we do here is start with an empty parent template and then
// build up a list of templates -- one for each file. Once all of the templates
// have been parsed, we loop through again and execute every template.
@@ -238,12 +270,11 @@ func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable)
t.Option("missingkey=zero")
}
e.initFunMap(t, referenceTpls)
e.initFunMap(t)
// We want to parse the templates in a predictable order. The order favors
// higher-level (in file system) templates over deeply nested templates.
keys := sortTemplates(tpls)
referenceKeys := sortTemplates(referenceTpls)
for _, filename := range keys {
r := tpls[filename]
@@ -252,17 +283,6 @@ func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable)
}
}
// Adding the reference templates to the template context
// so they can be referenced in the tpl function
for _, filename := range referenceKeys {
if t.Lookup(filename) == nil {
r := referenceTpls[filename]
if _, err := t.New(filename).Parse(r.tpl); err != nil {
return map[string]string{}, cleanupParseError(filename, err)
}
}
}
rendered = make(map[string]string, len(keys))
for _, filename := range keys {
// Don't render partials. We don't care out the direct output of partials.
@@ -391,6 +411,9 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.
newParentID := c.ChartFullPath()
for _, t := range c.Templates {
if t == nil {
continue
}
if !isTemplateValid(c, t.Name) {
continue
}

View File

@@ -99,7 +99,8 @@ func (f files) Glob(pattern string) files {
// The output will not be indented, so you will want to pipe this to the
// 'indent' template function.
//
// data:
// data:
//
// {{ .Files.Glob("config/**").AsConfig() | indent 4 }}
func (f files) AsConfig() string {
if f == nil {
@@ -128,8 +129,9 @@ func (f files) AsConfig() string {
// The output will not be indented, so you will want to pipe this to the
// 'indent' template function.
//
// data:
// {{ .Files.Glob("secrets/*").AsSecrets() }}
// data:
//
// {{ .Files.Glob("secrets/*").AsSecrets() | indent 4 }}
func (f files) AsSecrets() string {
if f == nil {
return ""
@@ -155,6 +157,9 @@ func (f files) Lines(path string) []string {
if f == nil || f[path] == nil {
return []string{}
}
return strings.Split(string(f[path]), "\n")
s := string(f[path])
if s[len(s)-1] == '\n' {
s = s[:len(s)-1]
}
return strings.Split(s, "\n")
}

View File

@@ -35,12 +35,11 @@ import (
//
// Known late-bound functions:
//
// - "include"
// - "tpl"
// - "include"
// - "tpl"
//
// These are late-bound in Engine.Render(). The
// version included in the FuncMap is a placeholder.
//
func funcMap() template.FuncMap {
f := sprig.TxtFuncMap()
delete(f, "env")

View File

@@ -39,9 +39,28 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam
// This function is considered deprecated, and will be renamed in Helm 4. It will no
// longer be a public function.
func NewLookupFunction(config *rest.Config) lookupFunc {
return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) {
return newLookupFunction(clientProviderFromConfig{config: config})
}
type ClientProvider interface {
// GetClientFor returns a dynamic.NamespaceableResourceInterface suitable for interacting with resources
// corresponding to the provided apiVersion and kind, as well as a boolean indicating whether the resources
// are namespaced.
GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error)
}
type clientProviderFromConfig struct {
config *rest.Config
}
func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) {
return getDynamicClientOnKind(apiVersion, kind, c.config)
}
func newLookupFunction(clientProvider ClientProvider) lookupFunc {
return func(apiversion string, kind string, namespace string, name string) (map[string]interface{}, error) {
var client dynamic.ResourceInterface
c, namespaced, err := getDynamicClientOnKind(apiversion, resource, config)
c, namespaced, err := clientProvider.GetClientFor(apiversion, kind)
if err != nil {
return map[string]interface{}{}, err
}

View File

@@ -13,7 +13,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package getter provides a generalize tool for fetching data by scheme.
/*
Package getter provides a generalize tool for fetching data by scheme.
This provides a method by which the plugin system can load arbitrary protocol
handlers based upon a URL scheme.

View File

@@ -37,6 +37,7 @@ type options struct {
caFile string
unTar bool
insecureSkipVerifyTLS bool
plainHTTP bool
username string
password string
passCredentialsAll bool
@@ -96,6 +97,12 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) Option {
}
}
func WithPlainHTTP(plainHTTP bool) Option {
return func(opts *options) {
opts.plainHTTP = plainHTTP
}
}
// WithTimeout sets the timeout for requests
func WithTimeout(timeout time.Duration) Option {
return func(opts *options) {
@@ -172,9 +179,21 @@ func (p Providers) ByScheme(scheme string) (Getter, error) {
return nil, errors.Errorf("scheme %q not supported", scheme)
}
const (
// The cost timeout references curl's default connection timeout.
// https://github.com/curl/curl/blob/master/lib/connect.h#L40C21-L40C21
// The helm commands are usually executed manually. Considering the acceptable waiting time, we reduced the entire request time to 120s.
DefaultHTTPTimeout = 120
)
var defaultOptions = []Option{WithTimeout(time.Second * DefaultHTTPTimeout)}
var httpProvider = Provider{
Schemes: []string{"http", "https"},
New: NewHTTPGetter,
New: func(options ...Option) (Getter, error) {
options = append(options, defaultOptions...)
return NewHTTPGetter(options...)
},
}
var ociProvider = Provider{

View File

@@ -123,8 +123,8 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) {
}
})
if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" {
tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile)
if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" || g.opts.insecureSkipVerifyTLS {
tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile, g.opts.insecureSkipVerifyTLS)
if err != nil {
return nil, errors.Wrap(err, "can't create TLS config for client")
}

View File

@@ -18,14 +18,22 @@ package getter
import (
"bytes"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
"helm.sh/helm/v3/internal/tlsutil"
"helm.sh/helm/v3/internal/urlutil"
"helm.sh/helm/v3/pkg/registry"
)
// OCIGetter is the default HTTP(/S) backend handler
type OCIGetter struct {
opts options
opts options
transport *http.Transport
once sync.Once
}
// Get performs a Get from repo.Getter and returns the body.
@@ -38,6 +46,15 @@ func (g *OCIGetter) Get(href string, options ...Option) (*bytes.Buffer, error) {
func (g *OCIGetter) get(href string) (*bytes.Buffer, error) {
client := g.opts.registryClient
// if the user has already provided a configured registry client, use it,
// this is particularly true when user has his own way of handling the client credentials.
if client == nil {
c, err := g.newRegistryClient()
if err != nil {
return nil, err
}
client = c
}
ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme))
@@ -63,18 +80,7 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) {
// NewOCIGetter constructs a valid http/https client as a Getter
func NewOCIGetter(ops ...Option) (Getter, error) {
registryClient, err := registry.NewClient(
registry.ClientOptEnableCache(true),
)
if err != nil {
return nil, err
}
client := OCIGetter{
opts: options{
registryClient: registryClient,
},
}
var client OCIGetter
for _, opt := range ops {
opt(&client.opts)
@@ -82,3 +88,68 @@ func NewOCIGetter(ops ...Option) (Getter, error) {
return &client, nil
}
func (g *OCIGetter) newRegistryClient() (*registry.Client, error) {
if g.opts.transport != nil {
client, err := registry.NewClient(
registry.ClientOptHTTPClient(&http.Client{
Transport: g.opts.transport,
Timeout: g.opts.timeout,
}),
)
if err != nil {
return nil, err
}
return client, nil
}
g.once.Do(func() {
g.transport = &http.Transport{
// From https://github.com/google/go-containerregistry/blob/31786c6cbb82d6ec4fb8eb79cd9387905130534e/pkg/v1/remote/options.go#L87
DisableCompression: true,
DialContext: (&net.Dialer{
// By default we wrap the transport in retries, so reduce the
// default dial timeout to 5s to avoid 5x 30s of connection
// timeouts when doing the "ping" on certain http registries.
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
})
if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" || g.opts.insecureSkipVerifyTLS {
tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile, g.opts.insecureSkipVerifyTLS)
if err != nil {
return nil, fmt.Errorf("can't create TLS config for client: %w", err)
}
sni, err := urlutil.ExtractHostname(g.opts.url)
if err != nil {
return nil, err
}
tlsConf.ServerName = sni
g.transport.TLSClientConfig = tlsConf
}
opts := []registry.ClientOption{registry.ClientOptHTTPClient(&http.Client{
Transport: g.transport,
Timeout: g.opts.timeout,
})}
if g.opts.plainHTTP {
opts = append(opts, registry.ClientOptPlainHTTP())
}
client, err := registry.NewClient(opts...)
if err != nil {
return nil, err
}
return client, nil
}

View File

@@ -17,6 +17,7 @@ package getter
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
@@ -62,6 +63,13 @@ type pluginGetter struct {
opts options
}
func (p *pluginGetter) setupOptionsEnv(env []string) []string {
env = append(env, fmt.Sprintf("HELM_PLUGIN_USERNAME=%s", p.opts.username))
env = append(env, fmt.Sprintf("HELM_PLUGIN_PASSWORD=%s", p.opts.password))
env = append(env, fmt.Sprintf("HELM_PLUGIN_PASS_CREDENTIALS_ALL=%t", p.opts.passCredentialsAll))
return env
}
// Get runs downloader plugin command
func (p *pluginGetter) Get(href string, options ...Option) (*bytes.Buffer, error) {
for _, opt := range options {
@@ -71,7 +79,7 @@ func (p *pluginGetter) Get(href string, options ...Option) (*bytes.Buffer, error
argv := append(commands[1:], p.opts.certFile, p.opts.keyFile, p.opts.caFile, href)
prog := exec.Command(filepath.Join(p.base, commands[0]), argv...)
plugin.SetupPluginEnv(p.settings, p.name, p.base)
prog.Env = os.Environ()
prog.Env = p.setupOptionsEnv(os.Environ())
buf := bytes.NewBuffer(nil)
prog.Stdout = buf
prog.Stderr = os.Stderr

View File

@@ -0,0 +1,68 @@
/*
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 ignore provides tools for writing ignore files (a la .gitignore).
This provides both an ignore parser and a file-aware processor.
The format of ignore files closely follows, but does not exactly match, the
format for .gitignore files (https://git-scm.com/docs/gitignore).
The formatting rules are as follows:
- Parsing is line-by-line
- Empty lines are ignored
- Lines the begin with # (comments) will be ignored
- Leading and trailing spaces are always ignored
- Inline comments are NOT supported ('foo* # Any foo' does not contain a comment)
- There is no support for multi-line patterns
- Shell glob patterns are supported. See Go's "path/filepath".Match
- If a pattern begins with a leading !, the match will be negated.
- If a pattern begins with a leading /, only paths relatively rooted will match.
- If the pattern ends with a trailing /, only directories will match
- If a pattern contains no slashes, file basenames are tested (not paths)
- The pattern sequence "**", while legal in a glob, will cause an error here
(to indicate incompatibility with .gitignore).
Example:
# Match any file named foo.txt
foo.txt
# Match any text file
*.txt
# Match only directories named mydir
mydir/
# Match only text files in the top-level directory
/*.txt
# Match only the file foo.txt in the top-level directory
/foo.txt
# Match any file named ab.txt, ac.txt, or ad.txt
a[b-d].txt
Notable differences from .gitignore:
- The '**' syntax is not supported.
- The globbing library is Go's 'filepath.Match', not fnmatch(3)
- Trailing spaces are always ignored (there is no supported escape sequence)
- The evaluation of escape sequences has not been tested for compatibility
- There is no support for '\!' as a special leading sequence.
*/
package ignore // import "helm.sh/helm/v3/pkg/ignore"

View File

@@ -37,6 +37,7 @@ import (
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
multierror "github.com/hashicorp/go-multierror"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -68,6 +69,14 @@ var ManagedFieldsManager string
// Client represents a client capable of communicating with the Kubernetes API.
type Client struct {
// Factory provides a minimal version of the kubectl Factory interface. If
// you need the full Factory you can type switch to the full interface.
// Since Kubernetes Go API does not provide backwards compatibility across
// minor versions, this API does not follow Helm backwards compatibility.
// Helm is exposing Kubernetes in this property and cannot guarantee this
// will not change. The minimal interface only has the functions that Helm
// needs. The smaller surface area of the interface means there is a lower
// chance of it changing.
Factory Factory
Log func(string, ...interface{})
// Namespace allows to bypass the kubeconfig file for the choice of the namespace
@@ -337,13 +346,7 @@ func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) {
validationDirective = metav1.FieldValidationStrict
}
dynamicClient, err := c.Factory.DynamicClient()
if err != nil {
return nil, err
}
verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
schema, err := c.Factory.Validator(validationDirective, verifier)
schema, err := c.Factory.Validator(validationDirective)
if err != nil {
return nil, err
}
@@ -363,13 +366,7 @@ func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, erro
validationDirective = metav1.FieldValidationStrict
}
dynamicClient, err := c.Factory.DynamicClient()
if err != nil {
return nil, err
}
verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
schema, err := c.Factory.Validator(validationDirective, verifier)
schema, err := c.Factory.Validator(validationDirective)
if err != nil {
return nil, err
}
@@ -456,7 +453,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy)
continue
}
if err := deleteResource(info); err != nil {
if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil {
c.Log("Failed to delete %q, err: %s", info.ObjectName(), err)
continue
}
@@ -465,17 +462,29 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
return res, nil
}
// Delete deletes Kubernetes resources specified in the resources list. It will
// attempt to delete all resources even if one or more fail and collect any
// errors. All successfully deleted items will be returned in the `Deleted`
// ResourceList that is part of the result.
// Delete deletes Kubernetes resources specified in the resources list with
// background cascade deletion. It will attempt to delete all resources even
// if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) Delete(resources ResourceList) (*Result, []error) {
return rdelete(c, resources, metav1.DeletePropagationBackground)
}
// Delete deletes Kubernetes resources specified in the resources list with
// given deletion propagation policy. It will attempt to delete all resources even
// if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) {
return rdelete(c, resources, policy)
}
func rdelete(c *Client, resources ResourceList, propagation metav1.DeletionPropagation) (*Result, []error) {
var errs []error
res := &Result{}
mtx := sync.Mutex{}
err := perform(resources, func(info *resource.Info) error {
c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind)
err := deleteResource(info)
err := deleteResource(info, propagation)
if err == nil || apierrors.IsNotFound(err) {
if err != nil {
c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err)
@@ -492,10 +501,8 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) {
return nil
})
if err != nil {
// Rewrite the message from "no objects visited" if that is what we got
// back
if err == ErrNoObjectsVisited {
err = errors.New("object not found, skipping delete")
if errors.Is(err, ErrNoObjectsVisited) {
err = fmt.Errorf("object not found, skipping delete: %w", err)
}
errs = append(errs, err)
}
@@ -532,6 +539,8 @@ func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration)
}
func perform(infos ResourceList, fn func(*resource.Info) error) error {
var result error
if len(infos) == 0 {
return ErrNoObjectsVisited
}
@@ -542,10 +551,11 @@ func perform(infos ResourceList, fn func(*resource.Info) error) error {
for range infos {
err := <-errs
if err != nil {
return err
result = multierror.Append(result, err)
}
}
return nil
return result
}
// getManagedFieldsManager returns the manager string. If one was set it will be returned.
@@ -593,8 +603,7 @@ func createResource(info *resource.Info) error {
return info.Refresh(obj, true)
}
func deleteResource(info *resource.Info) error {
policy := metav1.DeletePropagationBackground
func deleteResource(info *resource.Info, policy metav1.DeletionPropagation) error {
opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
_, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts)
return err
@@ -763,7 +772,7 @@ func (c *Client) waitForJob(obj runtime.Object, name string) (bool, error) {
if c.Type == batch.JobComplete && c.Status == "True" {
return true, nil
} else if c.Type == batch.JobFailed && c.Status == "True" {
return true, errors.Errorf("job failed: %s", c.Reason)
return true, errors.Errorf("job %s failed: %s", name, c.Reason)
}
}

View File

@@ -18,7 +18,6 @@ package kube // import "helm.sh/helm/v3/pkg/kube"
import (
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
@@ -27,6 +26,12 @@ import (
// Factory provides abstractions that allow the Kubectl command to be extended across multiple types
// of resources and different API sets.
// This interface is a minimal copy of the kubectl Factory interface containing only the functions
// needed by Helm. Since Kubernetes Go APIs, including interfaces, can change in any minor release
// this interface is not covered by the Helm backwards compatibility guarantee. The reasons for the
// minimal copy is that it does not include the full interface. Changes or additions to functions
// Helm does not need are not impacted or exposed. This minimizes the impact of Kubernetes changes
// being exposed.
type Factory interface {
// ToRawKubeConfigLoader return kubeconfig loader as-is
ToRawKubeConfigLoader() clientcmd.ClientConfig
@@ -42,7 +47,5 @@ type Factory interface {
NewBuilder() *resource.Builder
// Returns a schema that can validate objects stored on disk.
Validator(validationDirective string, verifier *resource.QueryParamVerifier) (validation.Schema, error)
// OpenAPIGetter returns a getter for the openapi schema document
OpenAPIGetter() discovery.OpenAPISchemaInterface
Validator(validationDirective string) (validation.Schema, error)
}

View File

@@ -22,6 +22,7 @@ import (
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
@@ -37,10 +38,12 @@ type FailingKubeClient struct {
GetError error
WaitError error
DeleteError error
DeleteWithPropagationError error
WatchUntilReadyError error
UpdateError error
BuildError error
BuildTableError error
BuildDummy bool
BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error
WaitDuration time.Duration
@@ -116,6 +119,9 @@ func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error
if f.BuildError != nil {
return []*resource.Info{}, f.BuildError
}
if f.BuildDummy {
return createDummyResourceList(), nil
}
return f.PrintingKubeClient.Build(r, false)
}
@@ -134,3 +140,21 @@ func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duratio
}
return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d)
}
// DeleteWithPropagationPolicy returns the configured error if set or prints
func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
if f.DeleteWithPropagationError != nil {
return nil, []error{f.DeleteWithPropagationError}
}
return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy)
}
func createDummyResourceList() kube.ResourceList {
var resInfo resource.Info
resInfo.Name = "dummyName"
resInfo.Namespace = "dummyNamespace"
var resourceList kube.ResourceList
resourceList.Append(&resInfo)
return resourceList
}

View File

@@ -22,6 +22,7 @@ import (
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource"
@@ -48,7 +49,7 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result,
return &kube.Result{Created: resources}, nil
}
func (p *PrintingKubeClient) Get(resources kube.ResourceList, related bool) (map[string][]runtime.Object, error) {
func (p *PrintingKubeClient) Get(resources kube.ResourceList, _ bool) (map[string][]runtime.Object, error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, err
@@ -115,6 +116,17 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Durati
return v1.PodSucceeded, nil
}
// DeleteWithPropagationPolicy implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}
}
return &kube.Result{Deleted: resources}, nil
}
func bufferize(resources kube.ResourceList) io.Reader {
var builder strings.Builder
for _, info := range resources {

View File

@@ -21,6 +21,7 @@ import (
"time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -79,6 +80,14 @@ type InterfaceExt interface {
WaitForDelete(resources ResourceList, timeout time.Duration) error
}
// InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface.
type InterfaceDeletionPropagation interface {
// Delete destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value.
DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error)
}
// InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface.
@@ -103,4 +112,5 @@ type InterfaceResources interface {
var _ Interface = (*Client)(nil)
var _ InterfaceExt = (*Client)(nil)
var _ InterfaceDeletionPropagation = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil)

View File

@@ -18,6 +18,7 @@ package kube // import "helm.sh/helm/v3/pkg/kube"
import (
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
@@ -83,19 +84,12 @@ type ReadyChecker struct {
// IsReady checks if v is ready. It supports checking readiness for pods,
// deployments, persistent volume claims, services, daemon sets, custom
// resource definitions, stateful sets, replication controllers, and replica
// sets. All other resource kinds are always considered ready.
// resource definitions, stateful sets, replication controllers, jobs (optional),
// and replica sets. All other resource kinds are always considered ready.
//
// IsReady will fetch the latest state of the object from the server prior to
// performing readiness checks, and it will return any error encountered.
func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, error) {
var (
// This defaults to true, otherwise we get to a point where
// things will always return false unless one of the objects
// that manages pods has been hit
ok = true
err error
)
switch value := AsVersioned(v).(type) {
case *corev1.Pod:
pod, err := c.client.CoreV1().Pods(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
@@ -105,9 +99,11 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err
case *batchv1.Job:
if c.checkJobs {
job, err := c.client.BatchV1().Jobs(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil || !c.jobReady(job) {
if err != nil {
return false, err
}
ready, err := c.jobReady(job)
return ready, err
}
case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment:
currentDeployment, err := c.client.AppsV1().Deployments(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
@@ -180,11 +176,30 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err
if !c.statefulSetReady(sts) {
return false, nil
}
case *corev1.ReplicationController, *extensionsv1beta1.ReplicaSet, *appsv1beta2.ReplicaSet, *appsv1.ReplicaSet:
ok, err = c.podsReadyForObject(ctx, v.Namespace, value)
}
if !ok || err != nil {
return false, err
case *corev1.ReplicationController:
rc, err := c.client.CoreV1().ReplicationControllers(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !c.replicationControllerReady(rc) {
return false, nil
}
ready, err := c.podsReadyForObject(ctx, v.Namespace, value)
if !ready || err != nil {
return false, err
}
case *extensionsv1beta1.ReplicaSet, *appsv1beta2.ReplicaSet, *appsv1.ReplicaSet:
rs, err := c.client.AppsV1().ReplicaSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !c.replicaSetReady(rs) {
return false, nil
}
ready, err := c.podsReadyForObject(ctx, v.Namespace, value)
if !ready || err != nil {
return false, err
}
}
return true, nil
}
@@ -222,16 +237,17 @@ func (c *ReadyChecker) isPodReady(pod *corev1.Pod) bool {
return false
}
func (c *ReadyChecker) jobReady(job *batchv1.Job) bool {
func (c *ReadyChecker) jobReady(job *batchv1.Job) (bool, error) {
if job.Status.Failed > *job.Spec.BackoffLimit {
c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName())
return false
// If a job is failed, it can't recover, so throw an error
return false, fmt.Errorf("job is failed: %s/%s", job.GetNamespace(), job.GetName())
}
if job.Spec.Completions != nil && job.Status.Succeeded < *job.Spec.Completions {
c.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName())
return false
return false, nil
}
return true
return true, nil
}
func (c *ReadyChecker) serviceReady(s *corev1.Service) bool {
@@ -272,6 +288,16 @@ func (c *ReadyChecker) volumeReady(v *corev1.PersistentVolumeClaim) bool {
}
func (c *ReadyChecker) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deployment) bool {
// Verify the replicaset readiness
if !c.replicaSetReady(rs) {
return false
}
// Verify the generation observed by the deployment controller matches the spec generation
if dep.Status.ObservedGeneration != dep.ObjectMeta.Generation {
c.log("Deployment is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", dep.Namespace, dep.Name, dep.Status.ObservedGeneration, dep.ObjectMeta.Generation)
return false
}
expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep)
if !(rs.Status.ReadyReplicas >= expectedReady) {
c.log("Deployment is not ready: %s/%s. %d out of %d expected pods are ready", dep.Namespace, dep.Name, rs.Status.ReadyReplicas, expectedReady)
@@ -281,6 +307,12 @@ func (c *ReadyChecker) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deploy
}
func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool {
// Verify the generation observed by the daemonSet controller matches the spec generation
if ds.Status.ObservedGeneration != ds.ObjectMeta.Generation {
c.log("DaemonSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", ds.Namespace, ds.Name, ds.Status.ObservedGeneration, ds.ObjectMeta.Generation)
return false
}
// If the update strategy is not a rolling update, there will be nothing to wait for
if ds.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType {
return true
@@ -351,22 +383,22 @@ func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool {
}
func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
// Verify the generation observed by the statefulSet controller matches the spec generation
if sts.Status.ObservedGeneration != sts.ObjectMeta.Generation {
c.log("StatefulSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", sts.Namespace, sts.Name, sts.Status.ObservedGeneration, sts.ObjectMeta.Generation)
return false
}
// If the update strategy is not a rolling update, there will be nothing to wait for
if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType {
c.log("StatefulSet skipped ready check: %s/%s. updateStrategy is %v", sts.Namespace, sts.Name, sts.Spec.UpdateStrategy.Type)
return true
}
// Make sure the status is up-to-date with the StatefulSet changes
if sts.Status.ObservedGeneration < sts.Generation {
c.log("StatefulSet is not ready: %s/%s. update has not yet been observed", sts.Namespace, sts.Name)
return false
}
// Dereference all the pointers because StatefulSets like them
var partition int
// 1 is the default for replicas if not set
var replicas = 1
replicas := 1
// For some reason, even if the update strategy is a rolling update, the
// actual rollingUpdate field can be nil. If it is, we can safely assume
// there is no partition value
@@ -393,8 +425,10 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
c.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods are ready", sts.Namespace, sts.Name, sts.Status.ReadyReplicas, replicas)
return false
}
if sts.Status.CurrentRevision != sts.Status.UpdateRevision {
// This check only makes sense when all partitions are being upgraded otherwise during a
// partioned rolling upgrade, this condition will never evaluate to true, leading to
// error.
if partition == 0 && sts.Status.CurrentRevision != sts.Status.UpdateRevision {
c.log("StatefulSet is not ready: %s/%s. currentRevision %s does not yet match updateRevision %s", sts.Namespace, sts.Name, sts.Status.CurrentRevision, sts.Status.UpdateRevision)
return false
}
@@ -403,6 +437,24 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
return true
}
func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationController) bool {
// Verify the generation observed by the replicationController controller matches the spec generation
if rc.Status.ObservedGeneration != rc.ObjectMeta.Generation {
c.log("ReplicationController is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", rc.Namespace, rc.Name, rc.Status.ObservedGeneration, rc.ObjectMeta.Generation)
return false
}
return true
}
func (c *ReadyChecker) replicaSetReady(rs *appsv1.ReplicaSet) bool {
// Verify the generation observed by the replicaSet controller matches the spec generation
if rs.Status.ObservedGeneration != rs.ObjectMeta.Generation {
c.log("ReplicaSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", rs.Namespace, rs.Name, rs.Status.ObservedGeneration, rs.ObjectMeta.Generation)
return false
}
return true
}
func getPods(ctx context.Context, client kubernetes.Interface, namespace, selector string) ([]corev1.Pod, error) {
list, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: selector,

View File

@@ -22,5 +22,6 @@ const ResourcePolicyAnno = "helm.sh/resource-policy"
// KeepPolicy is the resource policy type for keep
//
// This resource policy type allows resources to skip being deleted
// during an uninstallRelease action.
//
// during an uninstallRelease action.
const KeepPolicy = "keep"

View File

@@ -50,7 +50,7 @@ func (w *waiter) waitForResources(created ResourceList) error {
ctx, cancel := context.WithTimeout(context.Background(), w.timeout)
defer cancel()
return wait.PollImmediateUntil(2*time.Second, func() (bool, error) {
return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) {
for _, v := range created {
ready, err := w.c.IsReady(ctx, v)
if !ready || err != nil {
@@ -58,7 +58,7 @@ func (w *waiter) waitForResources(created ResourceList) error {
}
}
return true, nil
}, ctx.Done())
})
}
// waitForDeletedResources polls to check if all the resources are deleted or a timeout is reached
@@ -68,7 +68,7 @@ func (w *waiter) waitForDeletedResources(deleted ResourceList) error {
ctx, cancel := context.WithTimeout(context.Background(), w.timeout)
defer cancel()
return wait.PollImmediateUntil(2*time.Second, func() (bool, error) {
return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) {
for _, v := range deleted {
err := v.Get()
if err == nil || !apierrors.IsNotFound(err) {
@@ -76,7 +76,7 @@ func (w *waiter) waitForDeletedResources(deleted ResourceList) error {
}
}
return true, nil
}, ctx.Done())
})
}
// SelectorsForObject returns the pod label selector for a given object

View File

@@ -19,19 +19,25 @@ package lint // import "helm.sh/helm/v3/pkg/lint"
import (
"path/filepath"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint/rules"
"helm.sh/helm/v3/pkg/lint/support"
)
// All runs all of the available linters on the given base directory.
func All(basedir string, values map[string]interface{}, namespace string, strict bool) support.Linter {
func All(basedir string, values map[string]interface{}, namespace string, _ bool) support.Linter {
return AllWithKubeVersion(basedir, values, namespace, nil)
}
// AllWithKubeVersion runs all the available linters on the given base directory, allowing to specify the kubernetes version.
func AllWithKubeVersion(basedir string, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) support.Linter {
// Using abs path to get directory context
chartDir, _ := filepath.Abs(basedir)
linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.ValuesWithOverrides(&linter, values)
rules.Templates(&linter, values, namespace, strict)
rules.TemplatesWithKubeVersion(&linter, values, namespace, kubeVersion)
rules.Dependencies(&linter)
return linter
}

View File

@@ -18,7 +18,6 @@ package rules // import "helm.sh/helm/v3/pkg/lint/rules"
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -107,6 +106,10 @@ func validateChartName(cf *chart.Metadata) error {
if cf.Name == "" {
return errors.New("name is required")
}
name := filepath.Base(cf.Name)
if name != cf.Name {
return fmt.Errorf("chart name %q is invalid", cf.Name)
}
return nil
}
@@ -200,7 +203,7 @@ func validateChartType(cf *chart.Metadata) error {
// in a generic form of a map[string]interface{}, so that the type
// of the values can be checked
func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) {
b, err := ioutil.ReadFile(filename)
b, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

View File

@@ -37,6 +37,7 @@ func Dependencies(linter *support.Linter) {
}
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c))
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c))
linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c))
}
@@ -80,3 +81,23 @@ func validateDependencyInMetadata(c *chart.Chart) (err error) {
}
return err
}
func validateDependenciesUnique(c *chart.Chart) (err error) {
dependencies := map[string]*chart.Dependency{}
shadowing := []string{}
for _, dep := range c.Metadata.Dependencies {
key := dep.Name
if dep.Alias != "" {
key = dep.Alias
}
if dependencies[key] != nil {
shadowing = append(shadowing, key)
}
dependencies[key] = dep
}
if len(shadowing) > 0 {
err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ","))
}
return err
}

View File

@@ -24,6 +24,8 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/deprecation"
kscheme "k8s.io/client-go/kubernetes/scheme"
"helm.sh/helm/v3/pkg/chartutil"
)
var (
@@ -45,7 +47,7 @@ func (e deprecatedAPIError) Error() string {
return msg
}
func validateNoDeprecations(resource *K8sYamlStruct) error {
func validateNoDeprecations(resource *K8sYamlStruct, kubeVersion *chartutil.KubeVersion) error {
// if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation
if resource.APIVersion == "" {
return nil
@@ -54,6 +56,14 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
return nil
}
majorVersion := k8sVersionMajor
minorVersion := k8sVersionMinor
if kubeVersion != nil {
majorVersion = kubeVersion.Major
minorVersion = kubeVersion.Minor
}
runtimeObject, err := resourceToRuntimeObject(resource)
if err != nil {
// do not error for non-kubernetes resources
@@ -62,11 +72,12 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
}
return err
}
maj, err := strconv.Atoi(k8sVersionMajor)
maj, err := strconv.Atoi(majorVersion)
if err != nil {
return err
}
min, err := strconv.Atoi(k8sVersionMinor)
min, err := strconv.Atoi(minorVersion)
if err != nil {
return err
}

View File

@@ -45,7 +45,12 @@ var (
)
// Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool) {
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) {
TemplatesWithKubeVersion(linter, values, namespace, nil)
}
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version.
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) {
fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath)
@@ -70,9 +75,14 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
Namespace: namespace,
}
caps := chartutil.DefaultCapabilities.Copy()
if kubeVersion != nil {
caps.KubeVersion = *kubeVersion
}
// lint ignores import-values
// See https://github.com/helm/helm/issues/9658
if err := chartutil.ProcessDependencies(chart, values); err != nil {
if err := chartutil.ProcessDependenciesWithMerge(chart, values); err != nil {
return
}
@@ -80,7 +90,8 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
if err != nil {
return
}
valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, nil)
valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, caps)
if err != nil {
linter.RunLinterRule(support.ErrorSev, fpath, err)
return
@@ -141,15 +152,16 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
break
}
// If YAML linting fails, we sill progress. So we don't capture the returned state
// on this linter run.
linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err))
// If YAML linting fails here, it will always fail in the next block as well, so we should return here.
// fix https://github.com/helm/helm/issues/11391
if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) {
return
}
if yamlStruct != nil {
// NOTE: set to warnings to allow users to support out-of-date kubernetes
// Refs https://github.com/helm/helm/issues/8596
linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct))
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct))
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion))
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent))
@@ -187,10 +199,10 @@ func validateTopIndentLevel(content string) error {
// Validation functions
func validateTemplatesDir(templatesPath string) error {
if fi, err := os.Stat(templatesPath); err != nil {
return errors.New("directory not found")
} else if !fi.IsDir() {
return errors.New("not a directory")
if fi, err := os.Stat(templatesPath); err == nil {
if !fi.IsDir() {
return errors.New("not a directory")
}
}
return nil
}

View File

@@ -17,7 +17,6 @@ limitations under the License.
package rules
import (
"io/ioutil"
"os"
"path/filepath"
@@ -76,7 +75,7 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err
ext := filepath.Ext(valuesPath)
schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json"
schema, err := ioutil.ReadFile(schemaPath)
schema, err := os.ReadFile(schemaPath)
if len(schema) == 0 {
return nil
}

View File

@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package support contains tools for linting charts.
/*
Package support contains tools for linting charts.
Linting is the process of testing charts for errors or warnings regarding
formatting, compilation, or standards compliance.

View File

@@ -17,7 +17,6 @@ package plugin // import "helm.sh/helm/v3/pkg/plugin"
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
@@ -122,10 +121,10 @@ func getPlatformCommand(cmds []PlatformCommand) []string {
eq := strings.EqualFold
for _, c := range cmds {
if eq(c.OperatingSystem, runtime.GOOS) {
command = strings.Split(os.ExpandEnv(c.Command), " ")
command = strings.Split(c.Command, " ")
}
if eq(c.OperatingSystem, runtime.GOOS) && eq(c.Architecture, runtime.GOARCH) {
return strings.Split(os.ExpandEnv(c.Command), " ")
return strings.Split(c.Command, " ")
}
}
return command
@@ -149,16 +148,19 @@ func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) {
parts = getPlatformCommand(p.Metadata.PlatformCommand)
}
if platCmdLen == 0 || parts == nil {
parts = strings.Split(os.ExpandEnv(p.Metadata.Command), " ")
parts = strings.Split(p.Metadata.Command, " ")
}
if len(parts) == 0 || parts[0] == "" {
return "", nil, fmt.Errorf("no plugin command is applicable")
}
main := parts[0]
main := os.ExpandEnv(parts[0])
baseArgs := []string{}
if len(parts) > 1 {
baseArgs = parts[1:]
for _, cmdpart := range parts[1:] {
cmdexp := os.ExpandEnv(cmdpart)
baseArgs = append(baseArgs, cmdexp)
}
}
if !p.Metadata.IgnoreFlags {
baseArgs = append(baseArgs, extraArgs...)
@@ -173,6 +175,10 @@ var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$")
// validatePluginData validates a plugin's YAML data.
func validatePluginData(plug *Plugin, filepath string) error {
// When metadata section missing, initialize with no data
if plug.Metadata == nil {
plug.Metadata = &Metadata{}
}
if !validPluginName.MatchString(plug.Metadata.Name) {
return fmt.Errorf("invalid plugin name at %q", filepath)
}
@@ -216,7 +222,7 @@ func detectDuplicates(plugs []*Plugin) error {
// LoadDir loads a plugin from the given directory.
func LoadDir(dirname string) (*Plugin, error) {
pluginfile := filepath.Join(dirname, PluginFileName)
data, err := ioutil.ReadFile(pluginfile)
data, err := os.ReadFile(pluginfile)
if err != nil {
return nil, errors.Wrapf(err, "failed to read plugin at %q", pluginfile)
}

View File

@@ -13,7 +13,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package provenance provides tools for establishing the authenticity of a chart.
/*
Package provenance provides tools for establishing the authenticity of a chart.
In Helm, provenance is established via several factors. The primary factor is the
cryptographic signature of a chart. Chart authors may sign charts, which in turn

View File

@@ -20,7 +20,6 @@ import (
"crypto"
"encoding/hex"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@@ -42,9 +41,13 @@ var defaultPGPConfig = packet.Config{
// SumCollection represents a collection of file and image checksums.
//
// Files are of the form:
//
// FILENAME: "sha256:SUM"
//
// Images are of the form:
//
// "IMAGE:TAG": "sha256:SUM"
//
// Docker optionally supports sha512, and if this is the case, the hash marker
// will be 'sha512' instead of 'sha256'.
type SumCollection struct {
@@ -293,7 +296,7 @@ func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) {
}
func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) {
data, err := ioutil.ReadFile(filename)
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

View File

@@ -17,13 +17,16 @@ package pusher
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/tlsutil"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/registry"
)
@@ -59,8 +62,15 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
}
client := pusher.opts.registryClient
if client == nil {
c, err := pusher.newRegistryClient()
if err != nil {
return err
}
client = c
}
chartBytes, err := ioutil.ReadFile(chartRef)
chartBytes, err := os.ReadFile(chartRef)
if err != nil {
return err
}
@@ -68,7 +78,7 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
var pushOpts []registry.PushOption
provRef := fmt.Sprintf("%s.prov", chartRef)
if _, err := os.Stat(provRef); err == nil {
provBytes, err := ioutil.ReadFile(provRef)
provBytes, err := os.ReadFile(provRef)
if err != nil {
return err
}
@@ -85,18 +95,7 @@ func (pusher *OCIPusher) push(chartRef, href string) error {
// NewOCIPusher constructs a valid OCI client as a Pusher
func NewOCIPusher(ops ...Option) (Pusher, error) {
registryClient, err := registry.NewClient(
registry.ClientOptEnableCache(true),
)
if err != nil {
return nil, err
}
client := OCIPusher{
opts: options{
registryClient: registryClient,
},
}
var client OCIPusher
for _, opt := range ops {
opt(&client.opts)
@@ -104,3 +103,50 @@ func NewOCIPusher(ops ...Option) (Pusher, error) {
return &client, nil
}
func (pusher *OCIPusher) newRegistryClient() (*registry.Client, error) {
if (pusher.opts.certFile != "" && pusher.opts.keyFile != "") || pusher.opts.caFile != "" || pusher.opts.insecureSkipTLSverify {
tlsConf, err := tlsutil.NewClientTLS(pusher.opts.certFile, pusher.opts.keyFile, pusher.opts.caFile, pusher.opts.insecureSkipTLSverify)
if err != nil {
return nil, errors.Wrap(err, "can't create TLS config for client")
}
registryClient, err := registry.NewClient(
registry.ClientOptHTTPClient(&http.Client{
// From https://github.com/google/go-containerregistry/blob/31786c6cbb82d6ec4fb8eb79cd9387905130534e/pkg/v1/remote/options.go#L87
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
// By default we wrap the transport in retries, so reduce the
// default dial timeout to 5s to avoid 5x 30s of connection
// timeouts when doing the "ping" on certain http registries.
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
TLSClientConfig: tlsConf,
},
}),
registry.ClientOptEnableCache(true),
)
if err != nil {
return nil, err
}
return registryClient, nil
}
opts := []registry.ClientOption{registry.ClientOptEnableCache(true)}
if pusher.opts.plainHTTP {
opts = append(opts, registry.ClientOptPlainHTTP())
}
registryClient, err := registry.NewClient(opts...)
if err != nil {
return nil, err
}
return registryClient, nil
}

View File

@@ -27,7 +27,12 @@ import (
//
// Pushers may or may not ignore these parameters as they are passed in.
type options struct {
registryClient *registry.Client
registryClient *registry.Client
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
plainHTTP bool
}
// Option allows specifying various settings configurable by the user for overriding the defaults
@@ -41,6 +46,28 @@ func WithRegistryClient(client *registry.Client) Option {
}
}
// WithTLSClientConfig sets the client auth with the provided credentials.
func WithTLSClientConfig(certFile, keyFile, caFile string) Option {
return func(opts *options) {
opts.certFile = certFile
opts.keyFile = keyFile
opts.caFile = caFile
}
}
// WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked
func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) Option {
return func(opts *options) {
opts.insecureSkipTLSverify = insecureSkipTLSVerify
}
}
func WithPlainHTTP(plainHTTP bool) Option {
return func(opts *options) {
opts.plainHTTP = plainHTTP
}
}
// Pusher is an interface to support upload to the specified URL.
type Pusher interface {
// Push file content by url string
@@ -89,7 +116,7 @@ var ociProvider = Provider{
// All finds all of the registered pushers as a list of Provider instances.
// Currently, just the built-in pushers are collected.
func All(settings *cli.EnvSettings) Providers {
func All(_ *cli.EnvSettings) Providers {
result := Providers{ociProvider}
return result
}

View File

@@ -21,7 +21,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"sort"
"strings"
@@ -60,7 +59,9 @@ type (
out io.Writer
authorizer auth.Client
registryAuthorizer *registryauth.Client
resolver remotes.Resolver
resolver func(ref registry.Reference) (remotes.Resolver, error)
httpClient *http.Client
plainHTTP bool
}
// ClientOption allows specifying various settings configurable by the user for overriding the defaults
@@ -71,7 +72,7 @@ type (
// NewClient returns a new registry client with config
func NewClient(options ...ClientOption) (*Client, error) {
client := &Client{
out: ioutil.Discard,
out: io.Discard,
}
for _, option := range options {
option(client)
@@ -86,15 +87,29 @@ func NewClient(options ...ClientOption) (*Client, error) {
}
client.authorizer = authClient
}
if client.resolver == nil {
resolverFn := client.resolver // copy for avoiding recursive call
client.resolver = func(ref registry.Reference) (remotes.Resolver, error) {
if resolverFn != nil {
// validate if the resolverFn returns a valid resolver
if resolver, err := resolverFn(ref); resolver != nil && err == nil {
return resolver, nil
}
}
headers := http.Header{}
headers.Set("User-Agent", version.GetUserAgent())
opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)}
if client.httpClient != nil {
opts = append(opts, auth.WithResolverClient(client.httpClient))
}
if client.plainHTTP {
opts = append(opts, auth.WithResolverPlainHTTP())
}
resolver, err := client.authorizer.ResolverWithOpts(opts...)
if err != nil {
return nil, err
}
client.resolver = resolver
return resolver, nil
}
// allocate a cache if option is set
@@ -104,6 +119,7 @@ func NewClient(options ...ClientOption) (*Client, error) {
}
if client.registryAuthorizer == nil {
client.registryAuthorizer = &registryauth.Client{
Client: client.httpClient,
Header: http.Header{
"User-Agent": {version.GetUserAgent()},
},
@@ -166,6 +182,28 @@ func ClientOptCredentialsFile(credentialsFile string) ClientOption {
}
}
// ClientOptHTTPClient returns a function that sets the httpClient setting on a client options set
func ClientOptHTTPClient(httpClient *http.Client) ClientOption {
return func(client *Client) {
client.httpClient = httpClient
}
}
func ClientOptPlainHTTP() ClientOption {
return func(c *Client) {
c.plainHTTP = true
}
}
// ClientOptResolver returns a function that sets the resolver setting on a client options set
func ClientOptResolver(resolver remotes.Resolver) ClientOption {
return func(client *Client) {
client.resolver = func(ref registry.Reference) (remotes.Resolver, error) {
return resolver, nil
}
}
}
type (
// LoginOption allows specifying various settings on login
LoginOption func(*loginOperation)
@@ -174,6 +212,9 @@ type (
username string
password string
insecure bool
certFile string
keyFile string
caFile string
}
)
@@ -189,6 +230,7 @@ func (c *Client) Login(host string, options ...LoginOption) error {
auth.WithLoginUsername(operation.username),
auth.WithLoginSecret(operation.password),
auth.WithLoginUserAgent(version.GetUserAgent()),
auth.WithLoginTLS(operation.certFile, operation.keyFile, operation.caFile),
}
if operation.insecure {
authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure())
@@ -215,6 +257,15 @@ func LoginOptInsecure(insecure bool) LoginOption {
}
}
// LoginOptTLSClientConfig returns a function that sets the TLS settings on login.
func LoginOptTLSClientConfig(certFile, keyFile, caFile string) LoginOption {
return func(operation *loginOperation) {
operation.certFile = certFile
operation.keyFile = keyFile
operation.caFile = caFile
}
}
type (
// LogoutOption allows specifying various settings on logout
LogoutOption func(*logoutOperation)
@@ -241,21 +292,21 @@ type (
// PullResult is the result returned upon successful pull.
PullResult struct {
Manifest *descriptorPullSummary `json:"manifest"`
Config *descriptorPullSummary `json:"config"`
Chart *descriptorPullSummaryWithMeta `json:"chart"`
Prov *descriptorPullSummary `json:"prov"`
Manifest *DescriptorPullSummary `json:"manifest"`
Config *DescriptorPullSummary `json:"config"`
Chart *DescriptorPullSummaryWithMeta `json:"chart"`
Prov *DescriptorPullSummary `json:"prov"`
Ref string `json:"ref"`
}
descriptorPullSummary struct {
DescriptorPullSummary struct {
Data []byte `json:"-"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
descriptorPullSummaryWithMeta struct {
descriptorPullSummary
DescriptorPullSummaryWithMeta struct {
DescriptorPullSummary
Meta *chart.Metadata `json:"meta"`
}
@@ -300,7 +351,11 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
}
var descriptors, layers []ocispec.Descriptor
registryStore := content.Registry{Resolver: c.resolver}
remotesResolver, err := c.resolver(parsedRef)
if err != nil {
return nil, err
}
registryStore := content.Registry{Resolver: remotesResolver}
manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.String(), memoryStore, "",
oras.WithPullEmptyNameAllowed(),
@@ -354,16 +409,16 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) {
}
}
result := &PullResult{
Manifest: &descriptorPullSummary{
Manifest: &DescriptorPullSummary{
Digest: manifest.Digest.String(),
Size: manifest.Size,
},
Config: &descriptorPullSummary{
Config: &DescriptorPullSummary{
Digest: configDescriptor.Digest.String(),
Size: configDescriptor.Size,
},
Chart: &descriptorPullSummaryWithMeta{},
Prov: &descriptorPullSummary{},
Chart: &DescriptorPullSummaryWithMeta{},
Prov: &DescriptorPullSummary{},
Ref: parsedRef.String(),
}
var getManifestErr error
@@ -474,6 +529,7 @@ type (
pushOperation struct {
provData []byte
strictMode bool
test bool
}
)
@@ -527,7 +583,9 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
descriptors = append(descriptors, provDescriptor)
}
manifestData, manifest, err := content.GenerateManifest(&configDescriptor, nil, descriptors...)
ociAnnotations := generateOCIAnnotations(meta, operation.test)
manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...)
if err != nil {
return nil, err
}
@@ -536,7 +594,11 @@ func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResu
return nil, err
}
registryStore := content.Registry{Resolver: c.resolver}
remotesResolver, err := c.resolver(parsedRef)
if err != nil {
return nil, err
}
registryStore := content.Registry{Resolver: remotesResolver}
_, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.String(), registryStore, "",
oras.WithNameValidation(nil))
if err != nil {
@@ -590,6 +652,13 @@ func PushOptStrictMode(strictMode bool) PushOption {
}
}
// PushOptTest returns a function that sets whether test setting on push
func PushOptTest(test bool) PushOption {
return func(operation *pushOperation) {
operation.test = test
}
}
// Tags provides a sorted list all semver compliant tags for a given repository
func (c *Client) Tags(ref string) ([]string, error) {
parsedReference, err := registry.ParseReference(ref)
@@ -600,23 +669,14 @@ func (c *Client) Tags(ref string) ([]string, error) {
repository := registryremote.Repository{
Reference: parsedReference,
Client: c.registryAuthorizer,
PlainHTTP: c.plainHTTP,
}
var registryTags []string
for {
registryTags, err = registry.Tags(ctx(c.out, c.debug), &repository)
if err != nil {
// Fallback to http based request
if !repository.PlainHTTP && strings.Contains(err.Error(), "server gave HTTP response") {
repository.PlainHTTP = true
continue
}
return nil, err
}
break
registryTags, err = registry.Tags(ctx(c.out, c.debug), &repository)
if err != nil {
return nil, err
}
var tagVersions []*semver.Version

View File

@@ -21,18 +21,29 @@ import (
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
helmtime "helm.sh/helm/v3/pkg/time"
"github.com/Masterminds/semver/v3"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
orascontext "oras.land/oras-go/pkg/context"
"oras.land/oras-go/pkg/registry"
"helm.sh/helm/v3/internal/tlsutil"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
)
var immutableOciAnnotations = []string{
ocispec.AnnotationVersion,
ocispec.AnnotationTitle,
}
// IsOCI determines whether or not a URL is to be treated as an OCI URL
func IsOCI(url string) bool {
return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme))
@@ -129,3 +140,108 @@ func parseReference(raw string) (registry.Reference, error) {
return registry.ParseReference(raw)
}
// NewRegistryClientWithTLS is a helper function to create a new registry client with TLS enabled.
func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSverify bool, registryConfig string, debug bool) (*Client, error) {
tlsConf, err := tlsutil.NewClientTLS(certFile, keyFile, caFile, insecureSkipTLSverify)
if err != nil {
return nil, fmt.Errorf("can't create TLS config for client: %s", err)
}
// Create a new registry client
registryClient, err := NewClient(
ClientOptDebug(debug),
ClientOptEnableCache(true),
ClientOptWriter(out),
ClientOptCredentialsFile(registryConfig),
ClientOptHTTPClient(&http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConf,
},
}),
)
if err != nil {
return nil, err
}
return registryClient, nil
}
// generateOCIAnnotations will generate OCI annotations to include within the OCI manifest
func generateOCIAnnotations(meta *chart.Metadata, test bool) map[string]string {
// Get annotations from Chart attributes
ociAnnotations := generateChartOCIAnnotations(meta, test)
// Copy Chart annotations
annotations:
for chartAnnotationKey, chartAnnotationValue := range meta.Annotations {
// Avoid overriding key properties
for _, immutableOciKey := range immutableOciAnnotations {
if immutableOciKey == chartAnnotationKey {
continue annotations
}
}
// Add chart annotation
ociAnnotations[chartAnnotationKey] = chartAnnotationValue
}
return ociAnnotations
}
// getChartOCIAnnotations will generate OCI annotations from the provided chart
func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]string {
chartOCIAnnotations := map[string]string{}
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description)
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, meta.Name)
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version)
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home)
if !test {
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, helmtime.Now().UTC().Format(time.RFC3339))
}
if len(meta.Sources) > 0 {
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0])
}
if meta.Maintainers != nil && len(meta.Maintainers) > 0 {
var maintainerSb strings.Builder
for maintainerIdx, maintainer := range meta.Maintainers {
if len(maintainer.Name) > 0 {
maintainerSb.WriteString(maintainer.Name)
}
if len(maintainer.Email) > 0 {
maintainerSb.WriteString(" (")
maintainerSb.WriteString(maintainer.Email)
maintainerSb.WriteString(")")
}
if maintainerIdx < len(meta.Maintainers)-1 {
maintainerSb.WriteString(", ")
}
}
chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationAuthors, maintainerSb.String())
}
return chartOCIAnnotations
}
// addToMap takes an existing map and adds an item if the value is not empty
func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string {
// Add item to map if its
if len(strings.TrimSpace(newValue)) > 0 {
inputMap[newKey] = newValue
}
return inputMap
}

View File

@@ -29,6 +29,7 @@ type KindSortOrder []string
//
// Those occurring earlier in the list get installed before those occurring later in the list.
var InstallOrder KindSortOrder = []string{
"PriorityClass",
"Namespace",
"NetworkPolicy",
"ResourceQuota",
@@ -105,6 +106,7 @@ var UninstallOrder KindSortOrder = []string{
"ResourceQuota",
"NetworkPolicy",
"Namespace",
"PriorityClass",
}
// sort manifests by kind.
@@ -130,7 +132,7 @@ func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.H
return h
}
func lessByKind(a interface{}, b interface{}, kindA string, kindB string, o KindSortOrder) bool {
func lessByKind(_ interface{}, _ interface{}, kindA string, kindB string, o KindSortOrder) bool {
ordering := make(map[string]int, len(o))
for v, k := range o {
ordering[k] = v

View File

@@ -117,19 +117,19 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering
//
// To determine hook type, it looks for a YAML structure like this:
//
// kind: SomeKind
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook: pre-install
// kind: SomeKind
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook: pre-install
//
// To determine the policy to delete the hook, it looks for a YAML structure like this:
//
// kind: SomeKind
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook-delete-policy: hook-succeeded
// kind: SomeKind
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook-delete-policy: hook-succeeded
func (file *manifestFile) sort(result *result) error {
// Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys)
var sortedEntryKeys []string

View File

@@ -21,7 +21,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"io"
"log"
"net/url"
"os"
@@ -131,7 +131,7 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) {
return "", err
}
index, err := ioutil.ReadAll(resp)
index, err := io.ReadAll(resp)
if err != nil {
return "", err
}
@@ -148,12 +148,12 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) {
}
chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))
os.MkdirAll(filepath.Dir(chartsFile), 0755)
ioutil.WriteFile(chartsFile, []byte(charts.String()), 0644)
os.WriteFile(chartsFile, []byte(charts.String()), 0644)
// Create the index file in the cache directory
fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))
os.MkdirAll(filepath.Dir(fname), 0755)
return fname, ioutil.WriteFile(fname, index, 0644)
return fname, os.WriteFile(fname, index, 0644)
}
// Index generates an index for the chart repository and writes an index.yaml file.
@@ -170,7 +170,7 @@ func (r *ChartRepository) saveIndexFile() error {
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644)
return os.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644)
}
func (r *ChartRepository) generateIndex() error {

View File

@@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package repo implements the Helm Chart Repository.
/*
Package repo implements the Helm Chart Repository.
A chart repository is an HTTP server that provides information on charts. A local
repository cache is an on-disk representation of a chart repository.
@@ -83,9 +84,9 @@ The format of a repository.yaml file is:
This file maps three bits of information about a repository:
- The name the user uses to refer to it
- The fully qualified URL to the repository (index.yaml will be appended)
- The name of the local cachefile
- The name the user uses to refer to it
- The fully qualified URL to the repository (index.yaml will be appended)
- The name of the local cachefile
The format for both files was changed after Helm v2.0.0-Alpha.4. Helm is not
backwards compatible with those earlier versions.

View File

@@ -18,7 +18,7 @@ package repo
import (
"bytes"
"io/ioutil"
"encoding/json"
"log"
"os"
"path"
@@ -104,7 +104,7 @@ func NewIndexFile() *IndexFile {
// LoadIndexFile takes a file at the given path and returns an IndexFile object
func LoadIndexFile(path string) (*IndexFile, error) {
b, err := ioutil.ReadFile(path)
b, err := os.ReadFile(path)
if err != nil {
return nil, err
}
@@ -233,6 +233,18 @@ func (i IndexFile) WriteFile(dest string, mode os.FileMode) error {
return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode)
}
// WriteJSONFile writes an index file in JSON format to the given destination
// path.
//
// The mode on the file is set to 'mode'.
func (i IndexFile) WriteJSONFile(dest string, mode os.FileMode) error {
b, err := json.MarshalIndent(i, "", " ")
if err != nil {
return err
}
return fileutil.AtomicWriteFile(dest, bytes.NewReader(b), mode)
}
// Merge merges the given index file into this index.
//
// This merges by name and version.
@@ -337,7 +349,7 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
return i, ErrEmptyIndexYaml
}
if err := yaml.UnmarshalStrict(data, i); err != nil {
if err := jsonOrYamlUnmarshal(data, i); err != nil {
return i, err
}
@@ -347,6 +359,10 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source)
continue
}
// When metadata section missing, initialize with no data
if cvs[idx].Metadata == nil {
cvs[idx].Metadata = &chart.Metadata{}
}
if cvs[idx].APIVersion == "" {
cvs[idx].APIVersion = chart.APIVersionV1
}
@@ -362,3 +378,17 @@ func loadIndex(data []byte, source string) (*IndexFile, error) {
}
return i, nil
}
// jsonOrYamlUnmarshal unmarshals the given byte slice containing JSON or YAML
// into the provided interface.
//
// It automatically detects whether the data is in JSON or YAML format by
// checking its validity as JSON. If the data is valid JSON, it will use the
// `encoding/json` package to unmarshal it. Otherwise, it will use the
// `sigs.k8s.io/yaml` package to unmarshal the YAML data.
func jsonOrYamlUnmarshal(b []byte, i interface{}) error {
if json.Valid(b) {
return json.Unmarshal(b, i)
}
return yaml.UnmarshalStrict(b, i)
}

View File

@@ -17,7 +17,6 @@ limitations under the License.
package repo // import "helm.sh/helm/v3/pkg/repo"
import (
"io/ioutil"
"os"
"path/filepath"
"time"
@@ -47,7 +46,7 @@ func NewFile() *File {
// LoadFile takes a file at the given path and returns a File object
func LoadFile(path string) (*File, error) {
r := new(File)
b, err := ioutil.ReadFile(path)
b, err := os.ReadFile(path)
if err != nil {
return r, errors.Wrapf(err, "couldn't load repositories file (%s)", path)
}
@@ -122,5 +121,5 @@ func (r *File) WriteFile(path string, perm os.FileMode) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
return ioutil.WriteFile(path, data, perm)
return os.WriteFile(path, data, perm)
}

View File

@@ -78,6 +78,7 @@ func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) {
cfgmaps.Log("get: failed to decode data %q: %s", key, err)
return nil, err
}
r.Labels = filterSystemLabels(obj.ObjectMeta.Labels)
// return the release object
return r, nil
}
@@ -145,6 +146,7 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err
cfgmaps.Log("query: failed to decode release: %s", err)
continue
}
rls.Labels = item.ObjectMeta.Labels
results = append(results, rls)
}
return results, nil
@@ -157,6 +159,7 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error {
var lbs labels
lbs.init()
lbs.fromMap(rls.Labels)
lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix())))
// create a new configmap to hold the release
@@ -184,6 +187,7 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error {
var lbs labels
lbs.init()
lbs.fromMap(rls.Labels)
lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix())))
// create a new configmap object to hold the release
@@ -220,13 +224,12 @@ func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) {
//
// The following labels are used within each configmap:
//
// "modifiedAt" - timestamp indicating when this configmap was last modified. (set in Update)
// "createdAt" - timestamp indicating when this configmap was created. (set in Create)
// "version" - version of the release.
// "status" - status of the release (see pkg/release/status.go for variants)
// "owner" - owner of the configmap, currently "helm".
// "name" - name of the release.
//
// "modifiedAt" - timestamp indicating when this configmap was last modified. (set in Update)
// "createdAt" - timestamp indicating when this configmap was created. (set in Create)
// "version" - version of the release.
// "status" - status of the release (see pkg/release/status.go for variants)
// "owner" - owner of the configmap, currently "helm".
// "name" - name of the release.
func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigMap, error) {
const owner = "helm"
@@ -240,6 +243,9 @@ func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigM
lbs.init()
}
// apply custom labels
lbs.fromMap(rls.Labels)
// apply labels
lbs.set("name", rls.Name)
lbs.set("owner", owner)

View File

@@ -72,6 +72,7 @@ func (secrets *Secrets) Get(key string) (*rspb.Release, error) {
}
// found the secret, decode the base64 data string
r, err := decodeRelease(string(obj.Data["release"]))
r.Labels = filterSystemLabels(obj.ObjectMeta.Labels)
return r, errors.Wrapf(err, "get: failed to decode data %q", key)
}
@@ -136,6 +137,7 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error)
secrets.Log("query: failed to decode release: %s", err)
continue
}
rls.Labels = item.ObjectMeta.Labels
results = append(results, rls)
}
return results, nil
@@ -148,6 +150,7 @@ func (secrets *Secrets) Create(key string, rls *rspb.Release) error {
var lbs labels
lbs.init()
lbs.fromMap(rls.Labels)
lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix())))
// create a new secret to hold the release
@@ -173,6 +176,7 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error {
var lbs labels
lbs.init()
lbs.fromMap(rls.Labels)
lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix())))
// create a new secret object to hold the release
@@ -202,13 +206,12 @@ func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) {
//
// The following labels are used within each secret:
//
// "modifiedAt" - timestamp indicating when this secret was last modified. (set in Update)
// "createdAt" - timestamp indicating when this secret was created. (set in Create)
// "version" - version of the release.
// "status" - status of the release (see pkg/release/status.go for variants)
// "owner" - owner of the secret, currently "helm".
// "name" - name of the release.
//
// "modifiedAt" - timestamp indicating when this secret was last modified. (set in Update)
// "createdAt" - timestamp indicating when this secret was created. (set in Create)
// "version" - version of the release.
// "status" - status of the release (see pkg/release/status.go for variants)
// "owner" - owner of the secret, currently "helm".
// "name" - name of the release.
func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, error) {
const owner = "helm"
@@ -222,6 +225,9 @@ func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, er
lbs.init()
}
// apply custom labels
lbs.fromMap(rls.Labels)
// apply labels
lbs.set("name", rls.Name)
lbs.set("owner", owner)

View File

@@ -19,6 +19,7 @@ package driver // import "helm.sh/helm/v3/pkg/storage/driver"
import (
"fmt"
"sort"
"strconv"
"time"
"github.com/jmoiron/sqlx"
@@ -49,6 +50,7 @@ const postgreSQLDialect = "postgres"
const SQLDriverName = "SQL"
const sqlReleaseTableName = "releases_v1"
const sqlCustomLabelsTableName = "custom_labels_v1"
const (
sqlReleaseTableKeyColumn = "key"
@@ -61,6 +63,17 @@ const (
sqlReleaseTableOwnerColumn = "owner"
sqlReleaseTableCreatedAtColumn = "createdAt"
sqlReleaseTableModifiedAtColumn = "modifiedAt"
sqlCustomLabelsTableReleaseKeyColumn = "releaseKey"
sqlCustomLabelsTableReleaseNamespaceColumn = "releaseNamespace"
sqlCustomLabelsTableKeyColumn = "key"
sqlCustomLabelsTableValueColumn = "value"
)
// Following limits based on k8s labels limits - https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
const (
sqlCustomLabelsTableKeyMaxLenght = 253 + 1 + 63
sqlCustomLabelsTableValueMaxLenght = 63
)
const (
@@ -82,8 +95,42 @@ func (s *SQL) Name() string {
return SQLDriverName
}
// Check if all migrations al
func (s *SQL) checkAlreadyApplied(migrations []*migrate.Migration) bool {
// make map (set) of ids for fast search
migrationsIds := make(map[string]struct{})
for _, migration := range migrations {
migrationsIds[migration.Id] = struct{}{}
}
// get list of applied migrations
migrate.SetDisableCreateTable(true)
records, err := migrate.GetMigrationRecords(s.db.DB, postgreSQLDialect)
migrate.SetDisableCreateTable(false)
if err != nil {
s.Log("checkAlreadyApplied: failed to get migration records: %v", err)
return false
}
for _, record := range records {
if _, ok := migrationsIds[record.Id]; ok {
s.Log("checkAlreadyApplied: found previous migration (Id: %v) applied at %v", record.Id, record.AppliedAt)
delete(migrationsIds, record.Id)
}
}
// check if all migrations appliyed
if len(migrationsIds) != 0 {
for id := range migrationsIds {
s.Log("checkAlreadyApplied: find unapplied migration (id: %v)", id)
}
return false
}
return true
}
func (s *SQL) ensureDBSetup() error {
// Populate the database with the relations we need if they don't exist yet
migrations := &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
@@ -91,7 +138,7 @@ func (s *SQL) ensureDBSetup() error {
Up: []string{
fmt.Sprintf(`
CREATE TABLE %s (
%s VARCHAR(67),
%s VARCHAR(90),
%s VARCHAR(64) NOT NULL,
%s TEXT NOT NULL,
%s VARCHAR(64) NOT NULL,
@@ -109,9 +156,9 @@ func (s *SQL) ensureDBSetup() error {
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
CREATE INDEX ON %s (%s);
GRANT ALL ON %s TO PUBLIC;
ALTER TABLE %s ENABLE ROW LEVEL SECURITY;
`,
sqlReleaseTableName,
@@ -150,9 +197,50 @@ func (s *SQL) ensureDBSetup() error {
`, sqlReleaseTableName),
},
},
{
Id: "custom_labels",
Up: []string{
fmt.Sprintf(`
CREATE TABLE %s (
%s VARCHAR(64),
%s VARCHAR(67),
%s VARCHAR(%d),
%s VARCHAR(%d)
);
CREATE INDEX ON %s (%s, %s);
GRANT ALL ON %s TO PUBLIC;
ALTER TABLE %s ENABLE ROW LEVEL SECURITY;
`,
sqlCustomLabelsTableName,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableKeyMaxLenght,
sqlCustomLabelsTableValueColumn,
sqlCustomLabelsTableValueMaxLenght,
sqlCustomLabelsTableName,
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableName,
sqlCustomLabelsTableName,
),
},
Down: []string{
fmt.Sprintf(`
DELETE TABLE %s;
`, sqlCustomLabelsTableName),
},
},
},
}
// Check that init migration already applied
if s.checkAlreadyApplied(migrations.Migrations) {
return nil
}
// Populate the database with the relations we need if they don't exist yet
_, err := migrate.Exec(s.db.DB, postgreSQLDialect, migrations, migrate.Up)
return err
}
@@ -180,6 +268,13 @@ type SQLReleaseWrapper struct {
ModifiedAt int `db:"modifiedAt"`
}
type SQLReleaseCustomLabelWrapper struct {
ReleaseKey string `db:"release_key"`
ReleaseNamespace string `db:"release_namespace"`
Key string `db:"key"`
Value string `db:"value"`
}
// NewSQL initializes a new sql driver.
func NewSQL(connectionString string, logger func(string, ...interface{}), namespace string) (*SQL, error) {
db, err := sqlx.Connect(postgreSQLDialect, connectionString)
@@ -230,13 +325,18 @@ func (s *SQL) Get(key string) (*rspb.Release, error) {
return nil, err
}
if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil {
s.Log("failed to get release %s/%s custom labels: %v", s.namespace, key, err)
return nil, err
}
return release, nil
}
// List returns the list of all releases such that filter(release) == true
func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
sb := s.statementBuilder.
Select(sqlReleaseTableBodyColumn).
Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn).
From(sqlReleaseTableName).
Where(sq.Eq{sqlReleaseTableOwnerColumn: sqlReleaseDefaultOwner})
@@ -264,6 +364,15 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
s.Log("list: failed to decode release: %v: %v", record, err)
continue
}
if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil {
s.Log("failed to get release %s/%s custom labels: %v", record.Namespace, record.Key, err)
return nil, err
}
for k, v := range getReleaseSystemLabels(release) {
release.Labels[k] = v
}
if filter(release) {
releases = append(releases, release)
}
@@ -275,7 +384,7 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
// Query returns the set of releases that match the provided set of labels.
func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) {
sb := s.statementBuilder.
Select(sqlReleaseTableBodyColumn).
Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn).
From(sqlReleaseTableName)
keys := make([]string, 0, len(labels))
@@ -321,6 +430,12 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) {
s.Log("list: failed to decode release: %v: %v", record, err)
continue
}
if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil {
s.Log("failed to get release %s/%s custom labels: %v", record.Namespace, record.Key, err)
return nil, err
}
releases = append(releases, release)
}
@@ -403,6 +518,36 @@ func (s *SQL) Create(key string, rls *rspb.Release) error {
s.Log("failed to store release %s in SQL database: %v", key, err)
return err
}
// Filtering labels before insert cause in SQL storage driver system releases are stored in separate columns of release table
for k, v := range filterSystemLabels(rls.Labels) {
insertLabelsQuery, args, err := s.statementBuilder.
Insert(sqlCustomLabelsTableName).
Columns(
sqlCustomLabelsTableReleaseKeyColumn,
sqlCustomLabelsTableReleaseNamespaceColumn,
sqlCustomLabelsTableKeyColumn,
sqlCustomLabelsTableValueColumn,
).
Values(
key,
namespace,
k,
v,
).ToSql()
if err != nil {
defer transaction.Rollback()
s.Log("failed to build insert query: %v", err)
return err
}
if _, err := transaction.Exec(insertLabelsQuery, args...); err != nil {
defer transaction.Rollback()
s.Log("failed to write Labels: %v", err)
return err
}
}
defer transaction.Commit()
return nil
@@ -487,10 +632,66 @@ func (s *SQL) Delete(key string) (*rspb.Release, error) {
Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}).
ToSql()
if err != nil {
s.Log("failed to build select query: %v", err)
s.Log("failed to build delete query: %v", err)
return nil, err
}
_, err = transaction.Exec(deleteQuery, args...)
if err != nil {
s.Log("failed perform delete query: %v", err)
return release, err
}
if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil {
s.Log("failed to get release %s/%s custom labels: %v", s.namespace, key, err)
return nil, err
}
deleteCustomLabelsQuery, args, err := s.statementBuilder.
Delete(sqlCustomLabelsTableName).
Where(sq.Eq{sqlCustomLabelsTableReleaseKeyColumn: key}).
Where(sq.Eq{sqlCustomLabelsTableReleaseNamespaceColumn: s.namespace}).
ToSql()
if err != nil {
s.Log("failed to build delete Labels query: %v", err)
return nil, err
}
_, err = transaction.Exec(deleteCustomLabelsQuery, args...)
return release, err
}
// Get release custom labels from database
func (s *SQL) getReleaseCustomLabels(key string, _ string) (map[string]string, error) {
query, args, err := s.statementBuilder.
Select(sqlCustomLabelsTableKeyColumn, sqlCustomLabelsTableValueColumn).
From(sqlCustomLabelsTableName).
Where(sq.Eq{sqlCustomLabelsTableReleaseKeyColumn: key,
sqlCustomLabelsTableReleaseNamespaceColumn: s.namespace}).
ToSql()
if err != nil {
return nil, err
}
var labelsList = []SQLReleaseCustomLabelWrapper{}
if err := s.db.Select(&labelsList, query, args...); err != nil {
return nil, err
}
labelsMap := make(map[string]string)
for _, i := range labelsList {
labelsMap[i.Key] = i.Value
}
return filterSystemLabels(labelsMap), nil
}
// Rebuild system labels from release object
func getReleaseSystemLabels(rls *rspb.Release) map[string]string {
return map[string]string{
"name": rls.Name,
"owner": sqlReleaseDefaultOwner,
"status": rls.Info.Status.String(),
"version": strconv.Itoa(rls.Version),
}
}

View File

@@ -21,7 +21,7 @@ import (
"compress/gzip"
"encoding/base64"
"encoding/json"
"io/ioutil"
"io"
rspb "helm.sh/helm/v3/pkg/release"
)
@@ -30,6 +30,8 @@ var b64 = base64.StdEncoding
var magicGzip = []byte{0x1f, 0x8b, 0x08}
var systemLabels = []string{"name", "owner", "status", "version", "createdAt", "modifiedAt"}
// encodeRelease encodes a release returning a base64 encoded
// gzipped string representation, or error.
func encodeRelease(rls *rspb.Release) (string, error) {
@@ -69,7 +71,7 @@ func decodeRelease(data string) (*rspb.Release, error) {
return nil, err
}
defer r.Close()
b2, err := ioutil.ReadAll(r)
b2, err := io.ReadAll(r)
if err != nil {
return nil, err
}
@@ -83,3 +85,38 @@ func decodeRelease(data string) (*rspb.Release, error) {
}
return &rls, nil
}
// Checks if label is system
func isSystemLabel(key string) bool {
for _, v := range GetSystemLabels() {
if key == v {
return true
}
}
return false
}
// Removes system labels from labels map
func filterSystemLabels(lbs map[string]string) map[string]string {
result := make(map[string]string)
for k, v := range lbs {
if !isSystemLabel(k) {
result[k] = v
}
}
return result
}
// Checks if labels array contains system labels
func ContainsSystemLabels(lbs map[string]string) bool {
for k := range lbs {
if isSystemLabel(k) {
return true
}
}
return false
}
func GetSystemLabels() []string {
return systemLabels
}

View File

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

View File

@@ -1,561 +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 strvals
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
"unicode"
"github.com/pkg/errors"
"sigs.k8s.io/yaml"
)
// ErrNotList indicates that a non-list was treated as a list.
var ErrNotList = errors.New("not a list")
// MaxIndex is the maximum index that will be allowed by setIndex.
// The default value 65536 = 1024 * 64
var MaxIndex = 65536
// MaxNestedNameLevel is the maximum level of nesting for a value name that
// will be allowed.
var MaxNestedNameLevel = 30
// ToYAML takes a string of arguments and converts to a YAML document.
func ToYAML(s string) (string, error) {
m, err := Parse(s)
if err != nil {
return "", err
}
d, err := yaml.Marshal(m)
return strings.TrimSuffix(string(d), "\n"), err
}
// Parse parses a set line.
//
// A set line is of the form name1=value1,name2=value2
func Parse(s string) (map[string]interface{}, error) {
vals := map[string]interface{}{}
scanner := bytes.NewBufferString(s)
t := newParser(scanner, vals, false)
err := t.parse()
return vals, err
}
// ParseString parses a set line and forces a string value.
//
// A set line is of the form name1=value1,name2=value2
func ParseString(s string) (map[string]interface{}, error) {
vals := map[string]interface{}{}
scanner := bytes.NewBufferString(s)
t := newParser(scanner, vals, true)
err := t.parse()
return vals, err
}
// ParseInto parses a strvals line and merges the result into dest.
//
// If the strval string has a key that exists in dest, it overwrites the
// dest version.
func ParseInto(s string, dest map[string]interface{}) error {
scanner := bytes.NewBufferString(s)
t := newParser(scanner, dest, false)
return t.parse()
}
// ParseFile parses a set line, but its final value is loaded from the file at the path specified by the original value.
//
// A set line is of the form name1=path1,name2=path2
//
// When the files at path1 and path2 contained "val1" and "val2" respectively, the set line is consumed as
// name1=val1,name2=val2
func ParseFile(s string, reader RunesValueReader) (map[string]interface{}, error) {
vals := map[string]interface{}{}
scanner := bytes.NewBufferString(s)
t := newFileParser(scanner, vals, reader)
err := t.parse()
return vals, err
}
// ParseIntoString parses a strvals line and merges the result into dest.
//
// This method always returns a string as the value.
func ParseIntoString(s string, dest map[string]interface{}) error {
scanner := bytes.NewBufferString(s)
t := newParser(scanner, dest, true)
return t.parse()
}
// ParseJSON parses a string with format key1=val1, key2=val2, ...
// where values are json strings (null, or scalars, or arrays, or objects).
// An empty val is treated as null.
//
// If a key exists in dest, the new value overwrites the dest version.
//
func ParseJSON(s string, dest map[string]interface{}) error {
scanner := bytes.NewBufferString(s)
t := newJSONParser(scanner, dest)
return t.parse()
}
// ParseIntoFile parses a filevals line and merges the result into dest.
//
// This method always returns a string as the value.
func ParseIntoFile(s string, dest map[string]interface{}, reader RunesValueReader) error {
scanner := bytes.NewBufferString(s)
t := newFileParser(scanner, dest, reader)
return t.parse()
}
// RunesValueReader is a function that takes the given value (a slice of runes)
// and returns the parsed value
type RunesValueReader func([]rune) (interface{}, error)
// parser is a simple parser that takes a strvals line and parses it into a
// map representation.
//
// where sc is the source of the original data being parsed
// where data is the final parsed data from the parses with correct types
type parser struct {
sc *bytes.Buffer
data map[string]interface{}
reader RunesValueReader
isjsonval bool
}
func newParser(sc *bytes.Buffer, data map[string]interface{}, stringBool bool) *parser {
stringConverter := func(rs []rune) (interface{}, error) {
return typedVal(rs, stringBool), nil
}
return &parser{sc: sc, data: data, reader: stringConverter}
}
func newJSONParser(sc *bytes.Buffer, data map[string]interface{}) *parser {
return &parser{sc: sc, data: data, reader: nil, isjsonval: true}
}
func newFileParser(sc *bytes.Buffer, data map[string]interface{}, reader RunesValueReader) *parser {
return &parser{sc: sc, data: data, reader: reader}
}
func (t *parser) parse() error {
for {
err := t.key(t.data, 0)
if err == nil {
continue
}
if err == io.EOF {
return nil
}
return err
}
}
func runeSet(r []rune) map[rune]bool {
s := make(map[rune]bool, len(r))
for _, rr := range r {
s[rr] = true
}
return s
}
func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr error) {
defer func() {
if r := recover(); r != nil {
reterr = fmt.Errorf("unable to parse key: %s", r)
}
}()
stop := runeSet([]rune{'=', '[', ',', '.'})
for {
switch k, last, err := runesUntil(t.sc, stop); {
case err != nil:
if len(k) == 0 {
return err
}
return errors.Errorf("key %q has no value", string(k))
//set(data, string(k), "")
//return err
case last == '[':
// We are in a list index context, so we need to set an index.
i, err := t.keyIndex()
if err != nil {
return errors.Wrap(err, "error parsing index")
}
kk := string(k)
// Find or create target list
list := []interface{}{}
if _, ok := data[kk]; ok {
list = data[kk].([]interface{})
}
// Now we need to get the value after the ].
list, err = t.listItem(list, i, nestedNameLevel)
set(data, kk, list)
return err
case last == '=':
if t.isjsonval {
empval, err := t.emptyVal()
if err != nil {
return err
}
if empval {
set(data, string(k), nil)
return nil
}
// parse jsonvals by using Gos JSON standard library
// Decode is preferred to Unmarshal in order to parse just the json parts of the list key1=jsonval1,key2=jsonval2,...
// Since Decode has its own buffer that consumes more characters (from underlying t.sc) than the ones actually decoded,
// we invoke Decode on a separate reader built with a copy of what is left in t.sc. After Decode is executed, we
// discard in t.sc the chars of the decoded json value (the number of those characters is returned by InputOffset).
var jsonval interface{}
dec := json.NewDecoder(strings.NewReader(t.sc.String()))
if err = dec.Decode(&jsonval); err != nil {
return err
}
set(data, string(k), jsonval)
if _, err = io.CopyN(ioutil.Discard, t.sc, dec.InputOffset()); err != nil {
return err
}
// skip possible blanks and comma
_, err = t.emptyVal()
return err
}
//End of key. Consume =, Get value.
// FIXME: Get value list first
vl, e := t.valList()
switch e {
case nil:
set(data, string(k), vl)
return nil
case io.EOF:
set(data, string(k), "")
return e
case ErrNotList:
rs, e := t.val()
if e != nil && e != io.EOF {
return e
}
v, e := t.reader(rs)
set(data, string(k), v)
return e
default:
return e
}
case last == ',':
// No value given. Set the value to empty string. Return error.
set(data, string(k), "")
return errors.Errorf("key %q has no value (cannot end with ,)", string(k))
case last == '.':
// Check value name is within the maximum nested name level
nestedNameLevel++
if nestedNameLevel > MaxNestedNameLevel {
return fmt.Errorf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel)
}
// First, create or find the target map.
inner := map[string]interface{}{}
if _, ok := data[string(k)]; ok {
inner = data[string(k)].(map[string]interface{})
}
// Recurse
e := t.key(inner, nestedNameLevel)
if e == nil && len(inner) == 0 {
return errors.Errorf("key map %q has no value", string(k))
}
if len(inner) != 0 {
set(data, string(k), inner)
}
return e
}
}
}
func set(data map[string]interface{}, key string, val interface{}) {
// If key is empty, don't set it.
if len(key) == 0 {
return
}
data[key] = val
}
func setIndex(list []interface{}, index int, val interface{}) (l2 []interface{}, err error) {
// There are possible index values that are out of range on a target system
// causing a panic. This will catch the panic and return an error instead.
// The value of the index that causes a panic varies from system to system.
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("error processing index %d: %s", index, r)
}
}()
if index < 0 {
return list, fmt.Errorf("negative %d index not allowed", index)
}
if index > MaxIndex {
return list, fmt.Errorf("index of %d is greater than maximum supported index of %d", index, MaxIndex)
}
if len(list) <= index {
newlist := make([]interface{}, index+1)
copy(newlist, list)
list = newlist
}
list[index] = val
return list, nil
}
func (t *parser) keyIndex() (int, error) {
// First, get the key.
stop := runeSet([]rune{']'})
v, _, err := runesUntil(t.sc, stop)
if err != nil {
return 0, err
}
// v should be the index
return strconv.Atoi(string(v))
}
func (t *parser) listItem(list []interface{}, i, nestedNameLevel int) ([]interface{}, error) {
if i < 0 {
return list, fmt.Errorf("negative %d index not allowed", i)
}
stop := runeSet([]rune{'[', '.', '='})
switch k, last, err := runesUntil(t.sc, stop); {
case len(k) > 0:
return list, errors.Errorf("unexpected data at end of array index: %q", k)
case err != nil:
return list, err
case last == '=':
if t.isjsonval {
empval, err := t.emptyVal()
if err != nil {
return list, err
}
if empval {
return setIndex(list, i, nil)
}
// parse jsonvals by using Gos JSON standard library
// Decode is preferred to Unmarshal in order to parse just the json parts of the list key1=jsonval1,key2=jsonval2,...
// Since Decode has its own buffer that consumes more characters (from underlying t.sc) than the ones actually decoded,
// we invoke Decode on a separate reader built with a copy of what is left in t.sc. After Decode is executed, we
// discard in t.sc the chars of the decoded json value (the number of those characters is returned by InputOffset).
var jsonval interface{}
dec := json.NewDecoder(strings.NewReader(t.sc.String()))
if err = dec.Decode(&jsonval); err != nil {
return list, err
}
if list, err = setIndex(list, i, jsonval); err != nil {
return list, err
}
if _, err = io.CopyN(ioutil.Discard, t.sc, dec.InputOffset()); err != nil {
return list, err
}
// skip possible blanks and comma
_, err = t.emptyVal()
return list, err
}
vl, e := t.valList()
switch e {
case nil:
return setIndex(list, i, vl)
case io.EOF:
return setIndex(list, i, "")
case ErrNotList:
rs, e := t.val()
if e != nil && e != io.EOF {
return list, e
}
v, e := t.reader(rs)
if e != nil {
return list, e
}
return setIndex(list, i, v)
default:
return list, e
}
case last == '[':
// now we have a nested list. Read the index and handle.
nextI, err := t.keyIndex()
if err != nil {
return list, errors.Wrap(err, "error parsing index")
}
var crtList []interface{}
if len(list) > i {
// If nested list already exists, take the value of list to next cycle.
existed := list[i]
if existed != nil {
crtList = list[i].([]interface{})
}
}
// Now we need to get the value after the ].
list2, err := t.listItem(crtList, nextI, nestedNameLevel)
if err != nil {
return list, err
}
return setIndex(list, i, list2)
case last == '.':
// We have a nested object. Send to t.key
inner := map[string]interface{}{}
if len(list) > i {
var ok bool
inner, ok = list[i].(map[string]interface{})
if !ok {
// We have indices out of order. Initialize empty value.
list[i] = map[string]interface{}{}
inner = list[i].(map[string]interface{})
}
}
// Recurse
e := t.key(inner, nestedNameLevel)
if e != nil {
return list, e
}
return setIndex(list, i, inner)
default:
return nil, errors.Errorf("parse error: unexpected token %v", last)
}
}
// check for an empty value
// read and consume optional spaces until comma or EOF (empty val) or any other char (not empty val)
// comma and spaces are consumed, while any other char is not cosumed
func (t *parser) emptyVal() (bool, error) {
for {
r, _, e := t.sc.ReadRune()
if e == io.EOF {
return true, nil
}
if e != nil {
return false, e
}
if r == ',' {
return true, nil
}
if !unicode.IsSpace(r) {
t.sc.UnreadRune()
return false, nil
}
}
}
func (t *parser) val() ([]rune, error) {
stop := runeSet([]rune{','})
v, _, err := runesUntil(t.sc, stop)
return v, err
}
func (t *parser) valList() ([]interface{}, error) {
r, _, e := t.sc.ReadRune()
if e != nil {
return []interface{}{}, e
}
if r != '{' {
t.sc.UnreadRune()
return []interface{}{}, ErrNotList
}
list := []interface{}{}
stop := runeSet([]rune{',', '}'})
for {
switch rs, last, err := runesUntil(t.sc, stop); {
case err != nil:
if err == io.EOF {
err = errors.New("list must terminate with '}'")
}
return list, err
case last == '}':
// If this is followed by ',', consume it.
if r, _, e := t.sc.ReadRune(); e == nil && r != ',' {
t.sc.UnreadRune()
}
v, e := t.reader(rs)
list = append(list, v)
return list, e
case last == ',':
v, e := t.reader(rs)
if e != nil {
return list, e
}
list = append(list, v)
}
}
}
func runesUntil(in io.RuneReader, stop map[rune]bool) ([]rune, rune, error) {
v := []rune{}
for {
switch r, _, e := in.ReadRune(); {
case e != nil:
return v, r, e
case inMap(r, stop):
return v, r, nil
case r == '\\':
next, _, e := in.ReadRune()
if e != nil {
return v, next, e
}
v = append(v, next)
default:
v = append(v, r)
}
}
}
func inMap(k rune, m map[rune]bool) bool {
_, ok := m[k]
return ok
}
func typedVal(v []rune, st bool) interface{} {
val := string(v)
if st {
return val
}
if strings.EqualFold(val, "true") {
return true
}
if strings.EqualFold(val, "false") {
return false
}
if strings.EqualFold(val, "null") {
return nil
}
if strings.EqualFold(val, "0") {
return int64(0)
}
// If this value does not start with zero, try parsing it to an int
if len(val) != 0 && val[0] != '0' {
if iv, err := strconv.ParseInt(val, 10, 64); err == nil {
return iv
}
}
return val
}

View File

@@ -13,7 +13,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*Package uploader provides a library for uploading charts.
/*
Package uploader provides a library for uploading charts.
This package contains tools for uploading charts to registries.
*/