diff --git a/docs/release.md b/docs/release.md index d5b8d6c0cd..ec01705622 100644 --- a/docs/release.md +++ b/docs/release.md @@ -11,6 +11,9 @@ A Chart is a Helm package. It contains all of the resource definitions necessary resource "helm_release" "example" { name = "my_redis" chart = "redis" + values = [ + "${file("values.yaml")}" + ] } ``` @@ -23,7 +26,7 @@ The following arguments are supported: * `chart` - (Required) Chart name to be installed. * `devel` - (Optional) Use chart development versions, too. Equivalent to version '>0.0.0-0'. If version is set, this is ignored. * `version` - (Optional) Specify the exact chart version to install. If this is not specified, the latest version is installed. -* `values` - (Optional) Values in raw yaml file to pass to helm. +* `values` - (Optional) List of values in raw yaml to pass to helm. Values will be merged, in order, as Helm does with multiple `-f` options. * `set` - (Optional) Value block with custom values to be merge with the values.yaml. * `namespace` - (Optional) Namespace to install the release into. * `verify` - (Optional) Verify the package before installing it. @@ -55,7 +58,7 @@ The `metadata` block supports: * `revision` - Version is an int32 which represents the version of the release. * `status` - Status of the release. * `version` - A SemVer 2 conformant version string of the chart. - +* `values` - The compounded values from `values` and `set` ## Import diff --git a/helm/provider.go b/helm/provider.go index b59d8b9c58..e56d7ace47 100644 --- a/helm/provider.go +++ b/helm/provider.go @@ -187,6 +187,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { return NewMeta(d) } +// Meta is the meta information structure for the provider type Meta struct { Settings *helm_env.EnvSettings TLSConfig *tls.Config @@ -201,6 +202,7 @@ type Meta struct { sync.Mutex } +// NewMeta will construct a new Meta from the provided ResourceData func NewMeta(d *schema.ResourceData) (*Meta, error) { m := &Meta{data: d} m.buildSettings(m.data) @@ -328,6 +330,7 @@ func getK8sConfig(d *schema.ResourceData) (clientcmd.ClientConfig, error) { return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides), nil } +// GetHelmClient will return a new Helm client func (m *Meta) GetHelmClient() (helm.Interface, error) { if err := m.initialize(); err != nil { return nil, err diff --git a/helm/resource_release.go b/helm/resource_release.go index d0faf1299d..0e74b2d457 100644 --- a/helm/resource_release.go +++ b/helm/resource_release.go @@ -24,6 +24,7 @@ import ( "k8s.io/helm/pkg/strvals" ) +// ErrReleaseNotFound is the error when a Helm release is not found var ErrReleaseNotFound = errors.New("release not found") func resourceRelease() *schema.Resource { @@ -63,9 +64,10 @@ func resourceRelease() *schema.Resource { Description: "Use chart development versions, too. Equivalent to version '>0.0.0-0'. If version is set, this is ignored", }, "values": { - Type: schema.TypeString, + Type: schema.TypeList, Optional: true, - Description: "Values in raw yaml file to pass to helm.", + Description: "List of values in raw yaml file to pass to helm.", + Elem: &schema.Schema{Type: schema.TypeString}, }, "set": { Type: schema.TypeSet, @@ -168,6 +170,11 @@ func resourceRelease() *schema.Resource { Computed: true, Description: "A SemVer 2 conformant version string of the chart.", }, + "values": { + Type: schema.TypeString, + Computed: true, + Description: "The raw yaml values used for the chart.", + }, }, }, }, @@ -195,7 +202,7 @@ func prepareTillerForNewRelease(d *schema.ResourceData, c helm.Interface, name s switch r.Info.Status.GetCode() { case release.Status_DEPLOYED: - return setIdAndMetadataFromRelease(d, r) + return setIDAndMetadataFromRelease(d, r) case release.Status_FAILED: // delete and recreate it debug("release %s status is FAILED deleting it", name) @@ -266,7 +273,7 @@ func resourceReleaseCreate(d *schema.ResourceData, meta interface{}) error { return err } - return setIdAndMetadataFromRelease(d, res.Release) + return setIDAndMetadataFromRelease(d, res.Release) } func resourceReleaseRead(d *schema.ResourceData, meta interface{}) error { @@ -283,10 +290,12 @@ func resourceReleaseRead(d *schema.ResourceData, meta interface{}) error { return err } - return setIdAndMetadataFromRelease(d, r) + // d.Set("values_source_detected_md5", d.Get("values_sources_md5")) + + return setIDAndMetadataFromRelease(d, r) } -func setIdAndMetadataFromRelease(d *schema.ResourceData, r *release.Release) error { +func setIDAndMetadataFromRelease(d *schema.ResourceData, r *release.Release) error { d.SetId(r.Name) return d.Set("metadata", []map[string]interface{}{{ @@ -296,6 +305,7 @@ func setIdAndMetadataFromRelease(d *schema.ResourceData, r *release.Release) err "status": r.Info.Status.Code.String(), "chart": r.Chart.Metadata.Name, "version": r.Chart.Metadata.Version, + "values": r.Config.Raw, }}) } @@ -332,7 +342,7 @@ func resourceReleaseUpdate(d *schema.ResourceData, meta interface{}) error { return err } - return setIdAndMetadataFromRelease(d, res.Release) + return setIDAndMetadataFromRelease(d, res.Release) } func resourceReleaseDelete(d *schema.ResourceData, meta interface{}) error { @@ -427,13 +437,50 @@ func getChart(d *schema.ResourceData, m *Meta) (c *chart.Chart, path string, err return } +// Merges source and destination map, preferring values from the source map +// Taken from github.com/helm/cmd/install.go +func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { + for k, v := range src { + // If the key doesn't exist already, then just set the key to that value + if _, exists := dest[k]; !exists { + dest[k] = v + continue + } + nextMap, ok := v.(map[string]interface{}) + // If it isn't another map, overwrite the value + if !ok { + dest[k] = v + continue + } + // If the key doesn't exist already, then just set the key to that value + if _, exists := dest[k]; !exists { + dest[k] = nextMap + continue + } + // Edge case: If the key exists in the destination, but isn't a map + destMap, isMap := dest[k].(map[string]interface{}) + // If the source map has a map for this key, prefer it + if !isMap { + dest[k] = v + continue + } + // If we got to this point, it is a map in both, so merge them + dest[k] = mergeValues(destMap, nextMap) + } + return dest +} + func getValues(d *schema.ResourceData) ([]byte, error) { base := map[string]interface{}{} - values := d.Get("values").(string) - if values != "" { - if err := yaml.Unmarshal([]byte(values), &base); err != nil { - return nil, err + for _, raw := range d.Get("values").([]interface{}) { + values := raw.(string) + if values != "" { + currentMap := map[string]interface{}{} + if err := yaml.Unmarshal([]byte(values), ¤tMap); err != nil { + return nil, err + } + base = mergeValues(base, currentMap) } } diff --git a/helm/resource_release_test.go b/helm/resource_release_test.go index e4800d33e0..d4961266f9 100644 --- a/helm/resource_release_test.go +++ b/helm/resource_release_test.go @@ -3,6 +3,8 @@ package helm import ( "fmt" "regexp" + "strconv" + "strings" "sync" "testing" @@ -84,6 +86,54 @@ func TestAccResourceRelease_update(t *testing.T) { }) } +func TestAccResourceRelease_updateValues(t *testing.T) { + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testAccCheckHelmReleaseDestroy, + Steps: []resource.TestStep{{ + Config: testAccHelmReleaseConfigValues(testReleaseName, testNamespace, testReleaseName, "0.6.2", []string{"foo: bar"}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.revision", "1"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.version", "0.6.2"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.status", "DEPLOYED"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.values", "foo: bar\n"), + ), + }, { + Config: testAccHelmReleaseConfigValues(testReleaseName, testNamespace, testReleaseName, "0.6.2", []string{"foo: baz"}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.revision", "2"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.version", "0.6.2"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.status", "DEPLOYED"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.values", "foo: baz\n"), + ), + }}, + }) +} + +func TestAccResourceRelease_updateMultipleValues(t *testing.T) { + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + CheckDestroy: testAccCheckHelmReleaseDestroy, + Steps: []resource.TestStep{{ + Config: testAccHelmReleaseConfigValues(testReleaseName, testNamespace, testReleaseName, "0.6.2", []string{"foo: bar"}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.revision", "1"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.version", "0.6.2"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.status", "DEPLOYED"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.values", "foo: bar\n"), + ), + }, { + Config: testAccHelmReleaseConfigValues(testReleaseName, testNamespace, testReleaseName, "0.6.2", []string{"foo: bar", "foo:baz"}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.revision", "2"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.version", "0.6.2"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.status", "DEPLOYED"), + resource.TestCheckResourceAttr("helm_release.test", "metadata.0.values", "foo: baz\n"), + ), + }}, + }) +} + func TestAccResourceRelease_repository(t *testing.T) { resource.Test(t, resource.TestCase{ Providers: testAccProviders, @@ -177,9 +227,29 @@ func testAccHelmReleaseConfigBasic(resource, ns, name, version string) string { `, resource, name, ns, version) } +func testAccHelmReleaseConfigValues(resource, ns, name, version string, values []string) string { + vals := make([]string, len(values)) + for i, v := range values { + vals[i] = strconv.Quote(v) + } + return fmt.Sprintf(` + resource "helm_release" "%s" { + name = %q + namespace = %q + chart = "stable/mariadb" + version = %q + values = [ %q ] + } + `, resource, name, ns, version, strings.Join(vals, ",")) +} + func TestGetValues(t *testing.T) { d := resourceRelease().Data(nil) - d.Set("values", `foo: bar`) + d.Set("values", []string{ + "foo: bar\nbaz: corge", + "first: present\nbaz: grault", + "second: present\nbaz: uier", + }) d.Set("set", []interface{}{ map[string]interface{}{"name": "foo", "value": "qux"}, }) @@ -200,6 +270,15 @@ func TestGetValues(t *testing.T) { if base["foo"] != "qux" { t.Fatalf("error merging values, expected %q, got %q", "qux", base["foo"]) } + if base["first"] != "present" { + t.Fatalf("error merging values from file, expected value file %q not read", "testdata/get_values_first.yaml") + } + if base["second"] != "present" { + t.Fatalf("error merging values from file, expected value file %q not read", "testdata/get_values_second.yaml") + } + if base["baz"] != "uier" { + t.Fatalf("error merging values from file, expected %q, got %q", "uier", base["baz"]) + } } func testAccHelmReleaseConfigRepository(ns, name string) string { diff --git a/helm/resource_repository.go b/helm/resource_repository.go index 6344f1e210..2fd1a93d80 100644 --- a/helm/resource_repository.go +++ b/helm/resource_repository.go @@ -11,6 +11,7 @@ import ( "k8s.io/helm/pkg/repo" ) +// ErrRepositoryNotFound is the error when a Helm repository is not found var ErrRepositoryNotFound = errors.New("repository not found") func resourceRepository() *schema.Resource { @@ -102,7 +103,7 @@ func resourceRepositoryRead(d *schema.ResourceData, meta interface{}) error { return err } - return setIdAndMetadataFromRepository(d, r) + return setIDAndMetadataFromRepository(d, r) } func resourceRepositoryDelete(d *schema.ResourceData, meta interface{}) error { @@ -118,7 +119,7 @@ func resourceRepositoryDelete(d *schema.ResourceData, meta interface{}) error { return nil } -func setIdAndMetadataFromRepository(d *schema.ResourceData, r *repo.Entry) error { +func setIDAndMetadataFromRepository(d *schema.ResourceData, r *repo.Entry) error { d.SetId(r.Name) return d.Set("metadata", []map[string]interface{}{{ "name": r.Name,