...

Source file src/helm.sh/helm/v3/pkg/action/action.go

Documentation: helm.sh/helm/v3/pkg/action

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package action
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"regexp"
    26  	"strings"
    27  
    28  	"github.com/pkg/errors"
    29  	"k8s.io/apimachinery/pkg/api/meta"
    30  	"k8s.io/cli-runtime/pkg/genericclioptions"
    31  	"k8s.io/client-go/discovery"
    32  	"k8s.io/client-go/kubernetes"
    33  	"k8s.io/client-go/rest"
    34  
    35  	"helm.sh/helm/v3/pkg/chart"
    36  	"helm.sh/helm/v3/pkg/chartutil"
    37  	"helm.sh/helm/v3/pkg/engine"
    38  	"helm.sh/helm/v3/pkg/kube"
    39  	"helm.sh/helm/v3/pkg/postrender"
    40  	"helm.sh/helm/v3/pkg/registry"
    41  	"helm.sh/helm/v3/pkg/release"
    42  	"helm.sh/helm/v3/pkg/releaseutil"
    43  	"helm.sh/helm/v3/pkg/storage"
    44  	"helm.sh/helm/v3/pkg/storage/driver"
    45  	"helm.sh/helm/v3/pkg/time"
    46  )
    47  
    48  // Timestamper is a function capable of producing a timestamp.Timestamper.
    49  //
    50  // By default, this is a time.Time function from the Helm time package. This can
    51  // be overridden for testing though, so that timestamps are predictable.
    52  var Timestamper = time.Now
    53  
    54  var (
    55  	// errMissingChart indicates that a chart was not provided.
    56  	errMissingChart = errors.New("no chart provided")
    57  	// errMissingRelease indicates that a release (name) was not provided.
    58  	errMissingRelease = errors.New("no release provided")
    59  	// errInvalidRevision indicates that an invalid release revision number was provided.
    60  	errInvalidRevision = errors.New("invalid release revision")
    61  	// errPending indicates that another instance of Helm is already applying an operation on a release.
    62  	errPending = errors.New("another operation (install/upgrade/rollback) is in progress")
    63  )
    64  
    65  // ValidName is a regular expression for resource names.
    66  //
    67  // DEPRECATED: This will be removed in Helm 4, and is no longer used here. See
    68  // pkg/lint/rules.validateMetadataNameFunc for the replacement.
    69  //
    70  // According to the Kubernetes help text, the regular expression it uses is:
    71  //
    72  //	[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
    73  //
    74  // This follows the above regular expression (but requires a full string match, not partial).
    75  //
    76  // The Kubernetes documentation is here, though it is not entirely correct:
    77  // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
    78  var ValidName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
    79  
    80  // Configuration injects the dependencies that all actions share.
    81  type Configuration struct {
    82  	// RESTClientGetter is an interface that loads Kubernetes clients.
    83  	RESTClientGetter RESTClientGetter
    84  
    85  	// Releases stores records of releases.
    86  	Releases *storage.Storage
    87  
    88  	// KubeClient is a Kubernetes API client.
    89  	KubeClient kube.Interface
    90  
    91  	// RegistryClient is a client for working with registries
    92  	RegistryClient *registry.Client
    93  
    94  	// Capabilities describes the capabilities of the Kubernetes cluster.
    95  	Capabilities *chartutil.Capabilities
    96  
    97  	Log func(string, ...interface{})
    98  }
    99  
   100  // renderResources renders the templates in a chart
   101  //
   102  // TODO: This function is badly in need of a refactor.
   103  // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
   104  //
   105  //	This code has to do with writing files to disk.
   106  func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) {
   107  	hs := []*release.Hook{}
   108  	b := bytes.NewBuffer(nil)
   109  
   110  	caps, err := cfg.getCapabilities()
   111  	if err != nil {
   112  		return hs, b, "", err
   113  	}
   114  
   115  	if ch.Metadata.KubeVersion != "" {
   116  		if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
   117  			return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
   118  		}
   119  	}
   120  
   121  	var files map[string]string
   122  	var err2 error
   123  
   124  	// A `helm template` should not talk to the remote cluster. However, commands with the flag
   125  	//`--dry-run` with the value of `false`, `none`, or `server` should try to interact with the cluster.
   126  	// It may break in interesting and exotic ways because other data (e.g. discovery) is mocked.
   127  	if interactWithRemote && cfg.RESTClientGetter != nil {
   128  		restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
   129  		if err != nil {
   130  			return hs, b, "", err
   131  		}
   132  		e := engine.New(restConfig)
   133  		e.EnableDNS = enableDNS
   134  		files, err2 = e.Render(ch, values)
   135  	} else {
   136  		var e engine.Engine
   137  		e.EnableDNS = enableDNS
   138  		files, err2 = e.Render(ch, values)
   139  	}
   140  
   141  	if err2 != nil {
   142  		return hs, b, "", err2
   143  	}
   144  
   145  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   146  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   147  	// text file. We have to spin through this map because the file contains path information, so we
   148  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   149  	// it in the sortHooks.
   150  	var notesBuffer bytes.Buffer
   151  	for k, v := range files {
   152  		if strings.HasSuffix(k, notesFileSuffix) {
   153  			if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) {
   154  				// If buffer contains data, add newline before adding more
   155  				if notesBuffer.Len() > 0 {
   156  					notesBuffer.WriteString("\n")
   157  				}
   158  				notesBuffer.WriteString(v)
   159  			}
   160  			delete(files, k)
   161  		}
   162  	}
   163  	notes := notesBuffer.String()
   164  
   165  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   166  	// as partials are not used after renderer.Render. Empty manifests are also
   167  	// removed here.
   168  	hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
   169  	if err != nil {
   170  		// By catching parse errors here, we can prevent bogus releases from going
   171  		// to Kubernetes.
   172  		//
   173  		// We return the files as a big blob of data to help the user debug parser
   174  		// errors.
   175  		for name, content := range files {
   176  			if strings.TrimSpace(content) == "" {
   177  				continue
   178  			}
   179  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
   180  		}
   181  		return hs, b, "", err
   182  	}
   183  
   184  	// Aggregate all valid manifests into one big doc.
   185  	fileWritten := make(map[string]bool)
   186  
   187  	if includeCrds {
   188  		for _, crd := range ch.CRDObjects() {
   189  			if outputDir == "" {
   190  				fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Filename, string(crd.File.Data[:]))
   191  			} else {
   192  				err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Filename])
   193  				if err != nil {
   194  					return hs, b, "", err
   195  				}
   196  				fileWritten[crd.Filename] = true
   197  			}
   198  		}
   199  	}
   200  
   201  	for _, m := range manifests {
   202  		if outputDir == "" {
   203  			if hideSecret && m.Head.Kind == "Secret" && m.Head.Version == "v1" {
   204  				fmt.Fprintf(b, "---\n# Source: %s\n# HIDDEN: The Secret output has been suppressed\n", m.Name)
   205  			} else {
   206  				fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
   207  			}
   208  		} else {
   209  			newDir := outputDir
   210  			if useReleaseName {
   211  				newDir = filepath.Join(outputDir, releaseName)
   212  			}
   213  			// NOTE: We do not have to worry about the post-renderer because
   214  			// output dir is only used by `helm template`. In the next major
   215  			// release, we should move this logic to template only as it is not
   216  			// used by install or upgrade
   217  			err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
   218  			if err != nil {
   219  				return hs, b, "", err
   220  			}
   221  			fileWritten[m.Name] = true
   222  		}
   223  	}
   224  
   225  	if pr != nil {
   226  		b, err = pr.Run(b)
   227  		if err != nil {
   228  			return hs, b, notes, errors.Wrap(err, "error while running post render on files")
   229  		}
   230  	}
   231  
   232  	return hs, b, notes, nil
   233  }
   234  
   235  // RESTClientGetter gets the rest client
   236  type RESTClientGetter interface {
   237  	ToRESTConfig() (*rest.Config, error)
   238  	ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
   239  	ToRESTMapper() (meta.RESTMapper, error)
   240  }
   241  
   242  // DebugLog sets the logger that writes debug strings
   243  type DebugLog func(format string, v ...interface{})
   244  
   245  // capabilities builds a Capabilities from discovery information.
   246  func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
   247  	if cfg.Capabilities != nil {
   248  		return cfg.Capabilities, nil
   249  	}
   250  	dc, err := cfg.RESTClientGetter.ToDiscoveryClient()
   251  	if err != nil {
   252  		return nil, errors.Wrap(err, "could not get Kubernetes discovery client")
   253  	}
   254  	// force a discovery cache invalidation to always fetch the latest server version/capabilities.
   255  	dc.Invalidate()
   256  	kubeVersion, err := dc.ServerVersion()
   257  	if err != nil {
   258  		return nil, errors.Wrap(err, "could not get server version from Kubernetes")
   259  	}
   260  	// Issue #6361:
   261  	// Client-Go emits an error when an API service is registered but unimplemented.
   262  	// We trap that error here and print a warning. But since the discovery client continues
   263  	// building the API object, it is correctly populated with all valid APIs.
   264  	// See https://github.com/kubernetes/kubernetes/issues/72051#issuecomment-521157642
   265  	apiVersions, err := GetVersionSet(dc)
   266  	if err != nil {
   267  		if discovery.IsGroupDiscoveryFailedError(err) {
   268  			cfg.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err)
   269  			cfg.Log("WARNING: To fix this, kubectl delete apiservice <service-name>")
   270  		} else {
   271  			return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
   272  		}
   273  	}
   274  
   275  	cfg.Capabilities = &chartutil.Capabilities{
   276  		APIVersions: apiVersions,
   277  		KubeVersion: chartutil.KubeVersion{
   278  			Version: kubeVersion.GitVersion,
   279  			Major:   kubeVersion.Major,
   280  			Minor:   kubeVersion.Minor,
   281  		},
   282  		HelmVersion: chartutil.DefaultCapabilities.HelmVersion,
   283  	}
   284  	return cfg.Capabilities, nil
   285  }
   286  
   287  // KubernetesClientSet creates a new kubernetes ClientSet based on the configuration
   288  func (cfg *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
   289  	conf, err := cfg.RESTClientGetter.ToRESTConfig()
   290  	if err != nil {
   291  		return nil, errors.Wrap(err, "unable to generate config for kubernetes client")
   292  	}
   293  
   294  	return kubernetes.NewForConfig(conf)
   295  }
   296  
   297  // Now generates a timestamp
   298  //
   299  // If the configuration has a Timestamper on it, that will be used.
   300  // Otherwise, this will use time.Now().
   301  func (cfg *Configuration) Now() time.Time {
   302  	return Timestamper()
   303  }
   304  
   305  func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) {
   306  	if err := chartutil.ValidateReleaseName(name); err != nil {
   307  		return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name)
   308  	}
   309  
   310  	if version <= 0 {
   311  		return cfg.Releases.Last(name)
   312  	}
   313  
   314  	return cfg.Releases.Get(name, version)
   315  }
   316  
   317  // GetVersionSet retrieves a set of available k8s API versions
   318  func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) {
   319  	groups, resources, err := client.ServerGroupsAndResources()
   320  	if err != nil && !discovery.IsGroupDiscoveryFailedError(err) {
   321  		return chartutil.DefaultVersionSet, errors.Wrap(err, "could not get apiVersions from Kubernetes")
   322  	}
   323  
   324  	// FIXME: The Kubernetes test fixture for cli appears to always return nil
   325  	// for calls to Discovery().ServerGroupsAndResources(). So in this case, we
   326  	// return the default API list. This is also a safe value to return in any
   327  	// other odd-ball case.
   328  	if len(groups) == 0 && len(resources) == 0 {
   329  		return chartutil.DefaultVersionSet, nil
   330  	}
   331  
   332  	versionMap := make(map[string]interface{})
   333  	versions := []string{}
   334  
   335  	// Extract the groups
   336  	for _, g := range groups {
   337  		for _, gv := range g.Versions {
   338  			versionMap[gv.GroupVersion] = struct{}{}
   339  		}
   340  	}
   341  
   342  	// Extract the resources
   343  	var id string
   344  	var ok bool
   345  	for _, r := range resources {
   346  		for _, rl := range r.APIResources {
   347  
   348  			// A Kind at a GroupVersion can show up more than once. We only want
   349  			// it displayed once in the final output.
   350  			id = path.Join(r.GroupVersion, rl.Kind)
   351  			if _, ok = versionMap[id]; !ok {
   352  				versionMap[id] = struct{}{}
   353  			}
   354  		}
   355  	}
   356  
   357  	// Convert to a form that NewVersionSet can use
   358  	for k := range versionMap {
   359  		versions = append(versions, k)
   360  	}
   361  
   362  	return chartutil.VersionSet(versions), nil
   363  }
   364  
   365  // recordRelease with an update operation in case reuse has been set.
   366  func (cfg *Configuration) recordRelease(r *release.Release) {
   367  	if err := cfg.Releases.Update(r); err != nil {
   368  		cfg.Log("warning: Failed to update release %s: %s", r.Name, err)
   369  	}
   370  }
   371  
   372  // Init initializes the action configuration
   373  func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error {
   374  	kc := kube.New(getter)
   375  	kc.Log = log
   376  
   377  	lazyClient := &lazyClient{
   378  		namespace: namespace,
   379  		clientFn:  kc.Factory.KubernetesClientSet,
   380  	}
   381  
   382  	var store *storage.Storage
   383  	switch helmDriver {
   384  	case "secret", "secrets", "":
   385  		d := driver.NewSecrets(newSecretClient(lazyClient))
   386  		d.Log = log
   387  		store = storage.Init(d)
   388  	case "configmap", "configmaps":
   389  		d := driver.NewConfigMaps(newConfigMapClient(lazyClient))
   390  		d.Log = log
   391  		store = storage.Init(d)
   392  	case "memory":
   393  		var d *driver.Memory
   394  		if cfg.Releases != nil {
   395  			if mem, ok := cfg.Releases.Driver.(*driver.Memory); ok {
   396  				// This function can be called more than once (e.g., helm list --all-namespaces).
   397  				// If a memory driver was already initialized, re-use it but set the possibly new namespace.
   398  				// We re-use it in case some releases where already created in the existing memory driver.
   399  				d = mem
   400  			}
   401  		}
   402  		if d == nil {
   403  			d = driver.NewMemory()
   404  		}
   405  		d.SetNamespace(namespace)
   406  		store = storage.Init(d)
   407  	case "sql":
   408  		d, err := driver.NewSQL(
   409  			os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"),
   410  			log,
   411  			namespace,
   412  		)
   413  		if err != nil {
   414  			panic(fmt.Sprintf("Unable to instantiate SQL driver: %v", err))
   415  		}
   416  		store = storage.Init(d)
   417  	default:
   418  		// Not sure what to do here.
   419  		panic("Unknown driver in HELM_DRIVER: " + helmDriver)
   420  	}
   421  
   422  	cfg.RESTClientGetter = getter
   423  	cfg.KubeClient = kc
   424  	cfg.Releases = store
   425  	cfg.Log = log
   426  
   427  	return nil
   428  }
   429  

View as plain text