github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/action/action.go (about)

     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  	"github.com/stefanmcshane/helm/pkg/chart"
    36  	"github.com/stefanmcshane/helm/pkg/chartutil"
    37  	"github.com/stefanmcshane/helm/pkg/engine"
    38  	"github.com/stefanmcshane/helm/pkg/kube"
    39  	"github.com/stefanmcshane/helm/pkg/postrender"
    40  	"github.com/stefanmcshane/helm/pkg/registry"
    41  	"github.com/stefanmcshane/helm/pkg/release"
    42  	"github.com/stefanmcshane/helm/pkg/releaseutil"
    43  	"github.com/stefanmcshane/helm/pkg/storage"
    44  	"github.com/stefanmcshane/helm/pkg/storage/driver"
    45  	"github.com/stefanmcshane/helm/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  //       This code has to do with writing files to disk.
   105  func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) {
   106  	hs := []*release.Hook{}
   107  	b := bytes.NewBuffer(nil)
   108  
   109  	caps, err := cfg.getCapabilities()
   110  	if err != nil {
   111  		return hs, b, "", err
   112  	}
   113  
   114  	if ch.Metadata.KubeVersion != "" {
   115  		if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
   116  			return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
   117  		}
   118  	}
   119  
   120  	var files map[string]string
   121  	var err2 error
   122  
   123  	// A `helm template` or `helm install --dry-run` should not talk to the remote cluster.
   124  	// It will break in interesting and exotic ways because other data (e.g. discovery)
   125  	// is mocked. It is not up to the template author to decide when the user wants to
   126  	// connect to the cluster. So when the user says to dry run, respect the user's
   127  	// wishes and do not connect to the cluster.
   128  	if !dryRun && cfg.RESTClientGetter != nil {
   129  		restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
   130  		if err != nil {
   131  			return hs, b, "", err
   132  		}
   133  		files, err2 = engine.RenderWithClient(ch, values, restConfig)
   134  	} else {
   135  		files, err2 = engine.Render(ch, values)
   136  	}
   137  
   138  	if err2 != nil {
   139  		return hs, b, "", err2
   140  	}
   141  
   142  	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
   143  	// pull it out of here into a separate file so that we can actually use the output of the rendered
   144  	// text file. We have to spin through this map because the file contains path information, so we
   145  	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
   146  	// it in the sortHooks.
   147  	var notesBuffer bytes.Buffer
   148  	for k, v := range files {
   149  		if strings.HasSuffix(k, notesFileSuffix) {
   150  			if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) {
   151  				// If buffer contains data, add newline before adding more
   152  				if notesBuffer.Len() > 0 {
   153  					notesBuffer.WriteString("\n")
   154  				}
   155  				notesBuffer.WriteString(v)
   156  			}
   157  			delete(files, k)
   158  		}
   159  	}
   160  	notes := notesBuffer.String()
   161  
   162  	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
   163  	// as partials are not used after renderer.Render. Empty manifests are also
   164  	// removed here.
   165  	hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
   166  	if err != nil {
   167  		// By catching parse errors here, we can prevent bogus releases from going
   168  		// to Kubernetes.
   169  		//
   170  		// We return the files as a big blob of data to help the user debug parser
   171  		// errors.
   172  		for name, content := range files {
   173  			if strings.TrimSpace(content) == "" {
   174  				continue
   175  			}
   176  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
   177  		}
   178  		return hs, b, "", err
   179  	}
   180  
   181  	// Aggregate all valid manifests into one big doc.
   182  	fileWritten := make(map[string]bool)
   183  
   184  	if includeCrds {
   185  		for _, crd := range ch.CRDObjects() {
   186  			if outputDir == "" {
   187  				fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:]))
   188  			} else {
   189  				err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name])
   190  				if err != nil {
   191  					return hs, b, "", err
   192  				}
   193  				fileWritten[crd.Name] = true
   194  			}
   195  		}
   196  	}
   197  
   198  	for _, m := range manifests {
   199  		if outputDir == "" {
   200  			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
   201  		} else {
   202  			newDir := outputDir
   203  			if useReleaseName {
   204  				newDir = filepath.Join(outputDir, releaseName)
   205  			}
   206  			// NOTE: We do not have to worry about the post-renderer because
   207  			// output dir is only used by `helm template`. In the next major
   208  			// release, we should move this logic to template only as it is not
   209  			// used by install or upgrade
   210  			err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
   211  			if err != nil {
   212  				return hs, b, "", err
   213  			}
   214  			fileWritten[m.Name] = true
   215  		}
   216  	}
   217  
   218  	if pr != nil {
   219  		b, err = pr.Run(b)
   220  		if err != nil {
   221  			return hs, b, notes, errors.Wrap(err, "error while running post render on files")
   222  		}
   223  	}
   224  
   225  	return hs, b, notes, nil
   226  }
   227  
   228  // RESTClientGetter gets the rest client
   229  type RESTClientGetter interface {
   230  	ToRESTConfig() (*rest.Config, error)
   231  	ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
   232  	ToRESTMapper() (meta.RESTMapper, error)
   233  }
   234  
   235  // DebugLog sets the logger that writes debug strings
   236  type DebugLog func(format string, v ...interface{})
   237  
   238  // capabilities builds a Capabilities from discovery information.
   239  func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
   240  	if cfg.Capabilities != nil {
   241  		return cfg.Capabilities, nil
   242  	}
   243  	dc, err := cfg.RESTClientGetter.ToDiscoveryClient()
   244  	if err != nil {
   245  		return nil, errors.Wrap(err, "could not get Kubernetes discovery client")
   246  	}
   247  	// force a discovery cache invalidation to always fetch the latest server version/capabilities.
   248  	dc.Invalidate()
   249  	kubeVersion, err := dc.ServerVersion()
   250  	if err != nil {
   251  		return nil, errors.Wrap(err, "could not get server version from Kubernetes")
   252  	}
   253  	// Issue #6361:
   254  	// Client-Go emits an error when an API service is registered but unimplemented.
   255  	// We trap that error here and print a warning. But since the discovery client continues
   256  	// building the API object, it is correctly populated with all valid APIs.
   257  	// See https://github.com/kubernetes/kubernetes/issues/72051#issuecomment-521157642
   258  	apiVersions, err := GetVersionSet(dc)
   259  	if err != nil {
   260  		if discovery.IsGroupDiscoveryFailedError(err) {
   261  			cfg.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err)
   262  			cfg.Log("WARNING: To fix this, kubectl delete apiservice <service-name>")
   263  		} else {
   264  			return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes")
   265  		}
   266  	}
   267  
   268  	cfg.Capabilities = &chartutil.Capabilities{
   269  		APIVersions: apiVersions,
   270  		KubeVersion: chartutil.KubeVersion{
   271  			Version: kubeVersion.GitVersion,
   272  			Major:   kubeVersion.Major,
   273  			Minor:   kubeVersion.Minor,
   274  		},
   275  		HelmVersion: chartutil.DefaultCapabilities.HelmVersion,
   276  	}
   277  	return cfg.Capabilities, nil
   278  }
   279  
   280  // KubernetesClientSet creates a new kubernetes ClientSet based on the configuration
   281  func (cfg *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
   282  	conf, err := cfg.RESTClientGetter.ToRESTConfig()
   283  	if err != nil {
   284  		return nil, errors.Wrap(err, "unable to generate config for kubernetes client")
   285  	}
   286  
   287  	return kubernetes.NewForConfig(conf)
   288  }
   289  
   290  // Now generates a timestamp
   291  //
   292  // If the configuration has a Timestamper on it, that will be used.
   293  // Otherwise, this will use time.Now().
   294  func (cfg *Configuration) Now() time.Time {
   295  	return Timestamper()
   296  }
   297  
   298  func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) {
   299  	if err := chartutil.ValidateReleaseName(name); err != nil {
   300  		return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name)
   301  	}
   302  
   303  	if version <= 0 {
   304  		return cfg.Releases.Last(name)
   305  	}
   306  
   307  	return cfg.Releases.Get(name, version)
   308  }
   309  
   310  // GetVersionSet retrieves a set of available k8s API versions
   311  func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) {
   312  	groups, resources, err := client.ServerGroupsAndResources()
   313  	if err != nil && !discovery.IsGroupDiscoveryFailedError(err) {
   314  		return chartutil.DefaultVersionSet, errors.Wrap(err, "could not get apiVersions from Kubernetes")
   315  	}
   316  
   317  	// FIXME: The Kubernetes test fixture for cli appears to always return nil
   318  	// for calls to Discovery().ServerGroupsAndResources(). So in this case, we
   319  	// return the default API list. This is also a safe value to return in any
   320  	// other odd-ball case.
   321  	if len(groups) == 0 && len(resources) == 0 {
   322  		return chartutil.DefaultVersionSet, nil
   323  	}
   324  
   325  	versionMap := make(map[string]interface{})
   326  	versions := []string{}
   327  
   328  	// Extract the groups
   329  	for _, g := range groups {
   330  		for _, gv := range g.Versions {
   331  			versionMap[gv.GroupVersion] = struct{}{}
   332  		}
   333  	}
   334  
   335  	// Extract the resources
   336  	var id string
   337  	var ok bool
   338  	for _, r := range resources {
   339  		for _, rl := range r.APIResources {
   340  
   341  			// A Kind at a GroupVersion can show up more than once. We only want
   342  			// it displayed once in the final output.
   343  			id = path.Join(r.GroupVersion, rl.Kind)
   344  			if _, ok = versionMap[id]; !ok {
   345  				versionMap[id] = struct{}{}
   346  			}
   347  		}
   348  	}
   349  
   350  	// Convert to a form that NewVersionSet can use
   351  	for k := range versionMap {
   352  		versions = append(versions, k)
   353  	}
   354  
   355  	return chartutil.VersionSet(versions), nil
   356  }
   357  
   358  // recordRelease with an update operation in case reuse has been set.
   359  func (cfg *Configuration) recordRelease(r *release.Release) {
   360  	if err := cfg.Releases.Update(r); err != nil {
   361  		cfg.Log("warning: Failed to update release %s: %s", r.Name, err)
   362  	}
   363  }
   364  
   365  // Init initializes the action configuration
   366  func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error {
   367  	kc := kube.New(getter)
   368  	kc.Log = log
   369  
   370  	lazyClient := &lazyClient{
   371  		namespace: namespace,
   372  		clientFn:  kc.Factory.KubernetesClientSet,
   373  	}
   374  
   375  	var store *storage.Storage
   376  	switch helmDriver {
   377  	case "secret", "secrets", "":
   378  		d := driver.NewSecrets(newSecretClient(lazyClient))
   379  		d.Log = log
   380  		store = storage.Init(d)
   381  	case "configmap", "configmaps":
   382  		d := driver.NewConfigMaps(newConfigMapClient(lazyClient))
   383  		d.Log = log
   384  		store = storage.Init(d)
   385  	case "memory":
   386  		var d *driver.Memory
   387  		if cfg.Releases != nil {
   388  			if mem, ok := cfg.Releases.Driver.(*driver.Memory); ok {
   389  				// This function can be called more than once (e.g., helm list --all-namespaces).
   390  				// If a memory driver was already initialized, re-use it but set the possibly new namespace.
   391  				// We re-use it in case some releases where already created in the existing memory driver.
   392  				d = mem
   393  			}
   394  		}
   395  		if d == nil {
   396  			d = driver.NewMemory()
   397  		}
   398  		d.SetNamespace(namespace)
   399  		store = storage.Init(d)
   400  	case "sql":
   401  		d, err := driver.NewSQL(
   402  			os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"),
   403  			log,
   404  			namespace,
   405  		)
   406  		if err != nil {
   407  			panic(fmt.Sprintf("Unable to instantiate SQL driver: %v", err))
   408  		}
   409  		store = storage.Init(d)
   410  	default:
   411  		// Not sure what to do here.
   412  		panic("Unknown driver in HELM_DRIVER: " + helmDriver)
   413  	}
   414  
   415  	cfg.RESTClientGetter = getter
   416  	cfg.KubeClient = kc
   417  	cfg.Releases = store
   418  	cfg.Log = log
   419  
   420  	return nil
   421  }