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 }