github.com/pulumi/pulumi-kubernetes/sdk/v3@v3.30.2/go/kubernetes/helm/v2/chart.go (about)

     1  // Copyright 2016-2021, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // *** WARNING: this file was generated by pulumigen. ***
    16  // *** Do not edit by hand unless you're certain you know what you are doing! ***
    17  
    18  package helm
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"regexp"
    29  	"sort"
    30  	"strings"
    31  
    32  	"github.com/pkg/errors"
    33  	"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/yaml"
    34  	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    35  )
    36  
    37  // Chart is a component representing a collection of resources described by an arbitrary Helm
    38  // Chart. The Chart can be fetched from any source that is accessible to the `helm` command
    39  // line. Values in the `values.yml` file can be overridden using `ChartOpts.values` (equivalent
    40  // to `--set` or having multiple `values.yml` files). Objects can be transformed arbitrarily by
    41  // supplying callbacks to `ChartOpts.transformations`.
    42  //
    43  // `Chart` does not use Tiller. The Chart specified is copied and expanded locally; the semantics
    44  // are equivalent to running `helm template` and then using Pulumi to manage the resulting YAML
    45  // manifests. Any values that would be retrieved in-cluster are assigned fake values, and
    46  // none of Tiller's server-side validity testing is executed.
    47  //
    48  // ## Example Usage
    49  // ### Local Chart Directory
    50  //
    51  // ```go
    52  // package main
    53  //
    54  // import (
    55  //
    56  //	"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/helm/v3"
    57  //	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    58  //
    59  // )
    60  //
    61  //	func main() {
    62  //	    pulumi.Run(func(ctx *pulumi.Context) error {
    63  //	        _, err := helm.NewChart(ctx, "nginx-ingress", helm.ChartArgs{
    64  //	            Path: pulumi.String("./nginx-ingress"),
    65  //	        })
    66  //	        if err != nil {
    67  //	            return err
    68  //	        }
    69  //
    70  //	        return nil
    71  //	    })
    72  //	}
    73  //
    74  // ```
    75  // ### Remote Chart
    76  //
    77  // ```go
    78  // package main
    79  //
    80  // import (
    81  //
    82  //	"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/helm/v3"
    83  //	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    84  //
    85  // )
    86  //
    87  //	func main() {
    88  //	    pulumi.Run(func(ctx *pulumi.Context) error {
    89  //	        _, err := helm.NewChart(ctx, "nginx-ingress", helm.ChartArgs{
    90  //	            Chart:   pulumi.String("nginx-ingress"),
    91  //	            Version: pulumi.String("1.24.4"),
    92  //	            FetchArgs: helm.FetchArgs{
    93  //	                Repo: pulumi.String("https://charts.helm.sh/stable"),
    94  //	            },
    95  //	        })
    96  //	        if err != nil {
    97  //	            return err
    98  //	        }
    99  //
   100  //	        return nil
   101  //	    })
   102  //	}
   103  //
   104  // ```
   105  // ### Set Chart values
   106  //
   107  // ```go
   108  // package main
   109  //
   110  // import (
   111  //
   112  //	"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/helm/v3"
   113  //	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
   114  //
   115  // )
   116  //
   117  //	func main() {
   118  //	    pulumi.Run(func(ctx *pulumi.Context) error {
   119  //	        _, err := helm.NewChart(ctx, "nginx-ingress", helm.ChartArgs{
   120  //	            Chart:   pulumi.String("nginx-ingress"),
   121  //	            Version: pulumi.String("1.24.4"),
   122  //	            FetchArgs: helm.FetchArgs{
   123  //	                Repo: pulumi.String("https://charts.helm.sh/stable"),
   124  //	            },
   125  //	            Values: pulumi.Map{
   126  //	                "controller": pulumi.Map{
   127  //	                    "metrics": pulumi.Map{
   128  //	                        "enabled": pulumi.Bool(true),
   129  //	                    },
   130  //	                },
   131  //	            },
   132  //	        })
   133  //	        if err != nil {
   134  //	            return err
   135  //	        }
   136  //
   137  //	        return nil
   138  //	    })
   139  //	}
   140  //
   141  // ```
   142  // ### Deploy Chart into Namespace
   143  //
   144  // ```go
   145  // package main
   146  //
   147  // import (
   148  //
   149  //	"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/helm/v3"
   150  //	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
   151  //
   152  // )
   153  //
   154  //	func main() {
   155  //	    pulumi.Run(func(ctx *pulumi.Context) error {
   156  //	        _, err := helm.NewChart(ctx, "nginx-ingress", helm.ChartArgs{
   157  //	            Chart:     pulumi.String("nginx-ingress"),
   158  //	            Version:   pulumi.String("1.24.4"),
   159  //	            Namespace: pulumi.String("test-namespace"),
   160  //	            FetchArgs: helm.FetchArgs{
   161  //	                Repo: pulumi.String("https://charts.helm.sh/stable"),
   162  //	            },
   163  //	        })
   164  //	        if err != nil {
   165  //	            return err
   166  //	        }
   167  //
   168  //	        return nil
   169  //	    })
   170  //	}
   171  //
   172  // ```
   173  // ### Chart with Transformations
   174  //
   175  // ```go
   176  // package main
   177  //
   178  // import (
   179  //
   180  //	"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/helm/v3"
   181  //	"github.com/pulumi/pulumi-kubernetes/sdk/v3/go/kubernetes/yaml"
   182  //	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
   183  //
   184  // )
   185  //
   186  //	func main() {
   187  //	    pulumi.Run(func(ctx *pulumi.Context) error {
   188  //	        _, err := helm.NewChart(ctx, "nginx-ingress", helm.ChartArgs{
   189  //	            Chart:   pulumi.String("nginx-ingress"),
   190  //	            Version: pulumi.String("1.24.4"),
   191  //	            FetchArgs: helm.FetchArgs{
   192  //	                Repo: pulumi.String("https://charts.helm.sh/stable"),
   193  //	            },
   194  //	            Transformations: []yaml.Transformation{
   195  //	                // Make every service private to the cluster, i.e., turn all services into ClusterIP
   196  //	                // instead of LoadBalancer.
   197  //	                func(state map[string]interface{}, opts ...pulumi.ResourceOption) {
   198  //	                    if state["kind"] == "Service" {
   199  //	                        spec := state["spec"].(map[string]interface{})
   200  //	                        spec["type"] = "ClusterIP"
   201  //	                    }
   202  //	                },
   203  //
   204  //	                // Set a resource alias for a previous name.
   205  //	                func(state map[string]interface{}, opts ...pulumi.ResourceOption) {
   206  //	                    if state["kind"] == "Deployment" {
   207  //	                        aliases := pulumi.Aliases([]pulumi.Alias{
   208  //	                            {
   209  //	                                Name: pulumi.String("oldName"),
   210  //	                            },
   211  //	                        })
   212  //	                        opts = append(opts, aliases)
   213  //	                    }
   214  //	                },
   215  //
   216  //	                // Omit a resource from the Chart by transforming the specified resource definition
   217  //	                // to an empty List.
   218  //	                func(state map[string]interface{}, opts ...pulumi.ResourceOption) {
   219  //	                    name := state["metadata"].(map[string]interface{})["name"]
   220  //	                    if state["kind"] == "Pod" && name == "test" {
   221  //	                        state["apiVersion"] = "core/v1"
   222  //	                        state["kind"] = "List"
   223  //	                    }
   224  //	                },
   225  //	            },
   226  //	        })
   227  //	        if err != nil {
   228  //	            return err
   229  //	        }
   230  //
   231  //	        return nil
   232  //	    })
   233  //	}
   234  //
   235  // ```
   236  // Deprecated: helm/v2/Chart is deprecated by helm/v3/Chart and will be removed in a future release.
   237  type Chart struct {
   238  	pulumi.ResourceState
   239  
   240  	Ready     pulumi.ResourceArrayOutput
   241  	Resources pulumi.Output
   242  }
   243  
   244  // NewChart registers a new resource with the given unique name, arguments, and options.
   245  // Deprecated: helm/v2/Chart is deprecated by helm/v3/Chart and will be removed in a future release.
   246  func NewChart(ctx *pulumi.Context,
   247  	name string, args ChartArgs, opts ...pulumi.ResourceOption) (*Chart, error) {
   248  
   249  	// Register the resulting resource state.
   250  	chart := &Chart{}
   251  	err := ctx.RegisterComponentResource("kubernetes:helm.sh/v2:Chart", name, chart, opts...)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	// Honor the resource name prefix if specified.
   257  	if args.ResourcePrefix != "" {
   258  		name = args.ResourcePrefix + "-" + name
   259  	}
   260  
   261  	resources := args.ToChartArgsOutput().ApplyT(func(args chartArgs) (map[string]pulumi.Resource, error) {
   262  		return parseChart(ctx, name, args, pulumi.Parent(chart))
   263  	})
   264  	chart.Resources = resources
   265  
   266  	// Finally, register all of the resources found.
   267  	// Note: Go requires that we "pull" on our futures in order to get them scheduled for execution. Here, we use
   268  	// the engine's RegisterResourceOutputs to wait for the resolution of all resources that this Helm chart created.
   269  	err = ctx.RegisterResourceOutputs(chart, pulumi.Map{"resources": resources})
   270  	if err != nil {
   271  		return nil, errors.Wrap(err, "registering child resources")
   272  	}
   273  
   274  	chart.Ready = resources.ApplyT(func(x interface{}) []pulumi.Resource {
   275  		resources := x.(map[string]pulumi.Resource)
   276  		var outputs []pulumi.Resource
   277  		for _, r := range resources {
   278  			outputs = append(outputs, r)
   279  		}
   280  		return outputs
   281  	}).(pulumi.ResourceArrayOutput)
   282  
   283  	return chart, nil
   284  }
   285  
   286  func parseChart(ctx *pulumi.Context, name string, args chartArgs, opts ...pulumi.ResourceOption,
   287  ) (map[string]pulumi.Resource, error) {
   288  
   289  	// Create temporary directory and file to hold chart data and override values.
   290  	chartDir, err := ioutil.TempDir("", "")
   291  	if err != nil {
   292  		return nil, errors.Wrap(err, "creating temp directory for chart")
   293  	}
   294  	defer os.RemoveAll(chartDir)
   295  	overrides, err := ioutil.TempFile("", "values.*.yaml")
   296  	if err != nil {
   297  		return nil, errors.Wrap(err, "creating temp file for chart values")
   298  	}
   299  	defer os.Remove(overrides.Name())
   300  
   301  	var chart string
   302  	if args.Path != "" { // Local Chart
   303  		chart = args.Path
   304  	} else { // Remote Chart
   305  		if strings.HasPrefix(args.Repo, "http") {
   306  			return nil, fmt.Errorf("`repo` specifies the name of the Helm chart repo. Use FetchArgs.Repo" +
   307  				"to specify a URL")
   308  		}
   309  
   310  		chartToFetch := args.Chart
   311  		if len(args.Repo) > 0 {
   312  			chartToFetch = fmt.Sprintf("%s/%s", args.Repo, chartToFetch)
   313  		}
   314  
   315  		// Fetch the Chart.
   316  		if len(args.FetchArgs.Destination) == 0 {
   317  			args.FetchArgs.Destination = chartDir
   318  		}
   319  		if len(args.FetchArgs.Version) == 0 {
   320  			args.FetchArgs.Version = args.Version
   321  		}
   322  		err = fetch(chartToFetch, args.FetchArgs)
   323  		if err != nil {
   324  			return nil, err
   325  		}
   326  
   327  		// Get the path to the fetched Chart.
   328  		files, err := ioutil.ReadDir(chartDir)
   329  		if err != nil {
   330  			return nil, errors.Wrap(err, "failed to read chart directory")
   331  		}
   332  		if len(files) == 0 {
   333  			return nil, errors.New("chart directory was empty")
   334  		}
   335  		sort.Slice(files, func(i, j int) bool {
   336  			return files[i].Name() < files[j].Name()
   337  		})
   338  		fetchedChartName := files[0].Name()
   339  
   340  		chart = filepath.Join(chartDir, fetchedChartName)
   341  	}
   342  
   343  	defaultVals := filepath.Join(chart, "values.yaml")
   344  
   345  	helmArgs := []string{"template", chart, "--name-template", name, "--values", defaultVals}
   346  	// Write overrides file if Values set.
   347  	if args.Values != nil {
   348  		b, err := json.Marshal(args.Values)
   349  		if err != nil {
   350  			return nil, errors.Wrap(err, "failed to marshal overrides file")
   351  		}
   352  		_, err = overrides.Write(b)
   353  		if err != nil {
   354  			return nil, errors.Wrap(err, "failed to write overrides file")
   355  		}
   356  		helmArgs = append(helmArgs, "--values", overrides.Name())
   357  	}
   358  	if len(args.Namespace) > 0 {
   359  		helmArgs = append(helmArgs, "--namespace", args.Namespace)
   360  	}
   361  
   362  	for _, version := range args.APIVersions {
   363  		helmArgs = append(helmArgs, fmt.Sprintf("--api-versions=%s", version))
   364  	}
   365  
   366  	// Check for helm version
   367  	v3, err := isHelmV3()
   368  
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  
   373  	if v3 {
   374  		helmArgs = append(helmArgs, "--include-crds")
   375  	}
   376  
   377  	helmCmd := exec.Command("helm", helmArgs...)
   378  	var stderr bytes.Buffer
   379  	helmCmd.Stderr = &stderr
   380  	yamlBytes, err := helmCmd.Output()
   381  	if err != nil {
   382  		return nil, errors.Wrap(err, fmt.Sprintf("failed to run helm template: %s", stderr.String()))
   383  	}
   384  	objs, err := yamlDecode(ctx, string(yamlBytes), args.Namespace)
   385  	if err != nil {
   386  		return nil, err
   387  	}
   388  
   389  	resources, err := yaml.ParseYamlObjects(ctx, objs, args.Transformations, args.ResourcePrefix, opts...)
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  	return resources, nil
   394  }
   395  
   396  // yamlDecode invokes the function to decode a single YAML file and decompose it into object structures.
   397  func yamlDecode(ctx *pulumi.Context, text, namespace string) ([]map[string]interface{}, error) {
   398  	args := struct {
   399  		Text             string `pulumi:"text"`
   400  		DefaultNamespace string `pulumi:"defaultNamespace"`
   401  	}{Text: text, DefaultNamespace: namespace}
   402  	var ret struct {
   403  		Result []map[string]interface{} `pulumi:"result"`
   404  	}
   405  	if err := ctx.Invoke("kubernetes:yaml:decode", &args, &ret); err != nil {
   406  		return nil, errors.Wrap(err, "failed to decode YAML")
   407  	}
   408  	return ret.Result, nil
   409  }
   410  
   411  func isHelmV3() (bool, error) {
   412  
   413  	/*
   414  		Helm v2 returns version like this:
   415  		Client: v2.16.7+g5f2584f
   416  		Helm v3 returns a version like this:
   417  		v3.1.2+gd878d4d
   418  		--include-crds is available in helm v3.1+ so check for a regex matching that version
   419  	*/
   420  	helmVerArgs := []string{"version", "--short"}
   421  	helmVerCmd := exec.Command("helm", helmVerArgs...)
   422  
   423  	var stderr bytes.Buffer
   424  	helmVerCmd.Stderr = &stderr
   425  
   426  	version, err := helmVerCmd.Output()
   427  	if err != nil {
   428  		return false, errors.Wrap(err, fmt.Sprintf("failed to check helm version: %s", stderr.String()))
   429  	}
   430  
   431  	matched, err := regexp.MatchString(`^v3\.[1-9]`, string(version))
   432  	if err != nil {
   433  		return false, errors.Wrap(err, fmt.Sprintf("failed to perform regex match: %s", stderr.String()))
   434  	}
   435  
   436  	return matched, nil
   437  
   438  }
   439  
   440  func fetch(name string, args fetchArgs) error {
   441  	helmArgs := []string{"fetch", name}
   442  
   443  	// Untar by default.
   444  	if args.Untar == nil || !*args.Untar {
   445  		helmArgs = append(helmArgs, "--untar")
   446  	}
   447  
   448  	env := os.Environ()
   449  	// Helm v3 removed the `--home` flag, so we must use an env var instead.
   450  	if len(args.Home) > 0 {
   451  		found := false
   452  		for i, v := range env {
   453  			if strings.HasPrefix(v, "HELM_HOME=") {
   454  				env[i] = fmt.Sprintf("HELM_HOME=%s", args.Home)
   455  				found = true
   456  				break
   457  			}
   458  		}
   459  		if !found {
   460  			env = append(env, fmt.Sprintf("HELM_HOME=%s", args.Home))
   461  		}
   462  	}
   463  
   464  	if len(args.Version) > 0 {
   465  		helmArgs = append(helmArgs, "--version", args.Version)
   466  	}
   467  	if len(args.CAFile) > 0 {
   468  		helmArgs = append(helmArgs, "--ca-file", args.CAFile)
   469  	}
   470  	if len(args.CertFile) > 0 {
   471  		helmArgs = append(helmArgs, "--cert-file", args.CertFile)
   472  	}
   473  	if len(args.KeyFile) > 0 {
   474  		helmArgs = append(helmArgs, "--key-file", args.KeyFile)
   475  	}
   476  	if len(args.Destination) > 0 {
   477  		helmArgs = append(helmArgs, "--destination", args.Destination)
   478  	}
   479  	if len(args.Keyring) > 0 {
   480  		helmArgs = append(helmArgs, "--keyring", args.Keyring)
   481  	}
   482  	if len(args.Password) > 0 {
   483  		helmArgs = append(helmArgs, "--password", args.Password)
   484  	}
   485  	if len(args.Repo) > 0 {
   486  		helmArgs = append(helmArgs, "--repo", args.Repo)
   487  	}
   488  	if len(args.UntarDir) > 0 {
   489  		helmArgs = append(helmArgs, "--untardir", args.UntarDir)
   490  	}
   491  	if len(args.Username) > 0 {
   492  		helmArgs = append(helmArgs, "--username", args.Username)
   493  	}
   494  	if args.Devel != nil && *args.Devel {
   495  		helmArgs = append(helmArgs, "--devel")
   496  	}
   497  	if args.Prov != nil && *args.Prov {
   498  		helmArgs = append(helmArgs, "--prov")
   499  	}
   500  	if args.Verify != nil && *args.Verify {
   501  		helmArgs = append(helmArgs, "--verify")
   502  	}
   503  
   504  	helmCmd := exec.Command("helm", helmArgs...)
   505  	var stderr bytes.Buffer
   506  	helmCmd.Stderr = &stderr
   507  	err := helmCmd.Run()
   508  	if err != nil {
   509  		return errors.Wrap(err, fmt.Sprintf("failed to fetch Helm chart: %s", stderr.String()))
   510  	}
   511  
   512  	return nil
   513  }
   514  
   515  // GetResource returns a resource defined by a built-in Kubernetes group/version/kind, name and namespace.
   516  // For example, GetResource("v1/Pod", "foo", "") would return a Pod called "foo" from the "default" namespace.
   517  func (c *Chart) GetResource(gvk, name, namespace string) pulumi.AnyOutput {
   518  	id := name
   519  	if len(namespace) > 0 && namespace != "default" {
   520  		id = fmt.Sprintf("%s/%s", namespace, name)
   521  	}
   522  	key := fmt.Sprintf("%s::%s", gvk, id)
   523  	return c.Resources.ApplyT(func(x interface{}) interface{} {
   524  		resources := x.(map[string]pulumi.Resource)
   525  		return resources[key]
   526  	}).(pulumi.AnyOutput)
   527  }