diff --git a/pkg/helm/fake.go b/pkg/helm/fake.go index a3b0ebc84..ffb5b40c9 100644 --- a/pkg/helm/fake.go +++ b/pkg/helm/fake.go @@ -17,23 +17,29 @@ limitations under the License. package helm // import "k8s.io/helm/pkg/helm" import ( + "bytes" "errors" "math/rand" + "strings" "sync" "github.com/golang/protobuf/ptypes/timestamp" + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/manifest" "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" rls "k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/proto/hapi/version" + "k8s.io/helm/pkg/renderutil" storage "k8s.io/helm/pkg/storage/driver" ) // FakeClient implements Interface type FakeClient struct { - Rels []*release.Release - Responses map[string]release.TestRun_Status - Opts options + Rels []*release.Release + Responses map[string]release.TestRun_Status + Opts options + RenderManifests bool } // Option returns the fake release client @@ -96,7 +102,22 @@ func (c *FakeClient) InstallReleaseFromChart(chart *chart.Chart, ns string, opts return nil, errors.New("cannot re-use a name that is still in use") } - release := ReleaseMock(&MockReleaseOptions{Name: releaseName, Namespace: ns, Description: releaseDescription}) + mockOpts := &MockReleaseOptions{ + Name: releaseName, + Chart: chart, + Config: c.Opts.instReq.Values, + Namespace: ns, + Description: releaseDescription, + } + + release := ReleaseMock(mockOpts) + + if c.RenderManifests { + if err := RenderReleaseMock(release, false); err != nil { + return nil, err + } + } + if !c.Opts.dryRun { c.Rels = append(c.Rels, release) } @@ -135,14 +156,44 @@ func (c *FakeClient) UpdateRelease(rlsName string, chStr string, opts ...UpdateO } // UpdateReleaseFromChart returns an UpdateReleaseResponse containing the updated release, if it exists -func (c *FakeClient) UpdateReleaseFromChart(rlsName string, chart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { +func (c *FakeClient) UpdateReleaseFromChart(rlsName string, newChart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) { + for _, opt := range opts { + opt(&c.Opts) + } // Check to see if the release already exists. rel, err := c.ReleaseContent(rlsName, nil) if err != nil { return nil, err } - return &rls.UpdateReleaseResponse{Release: rel.Release}, nil + mockOpts := &MockReleaseOptions{ + Name: rel.Release.Name, + Version: rel.Release.Version + 1, + Chart: newChart, + Config: c.Opts.updateReq.Values, + Namespace: rel.Release.Namespace, + Description: c.Opts.updateReq.Description, + } + + newRelease := ReleaseMock(mockOpts) + + if c.Opts.updateReq.ResetValues { + newRelease.Config = &chart.Config{Raw: "{}"} + } else if c.Opts.updateReq.ReuseValues { + // TODO: This should merge old and new values but does not. + } + + if c.RenderManifests { + if err := RenderReleaseMock(newRelease, true); err != nil { + return nil, err + } + } + + if !c.Opts.dryRun { + *rel.Release = *newRelease + } + + return &rls.UpdateReleaseResponse{Release: newRelease}, nil } // RollbackRelease returns nil, nil @@ -231,12 +282,15 @@ type MockReleaseOptions struct { Name string Version int32 Chart *chart.Chart + Config *chart.Config StatusCode release.Status_Code Namespace string Description string } -// ReleaseMock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. +// ReleaseMock creates a mock release object based on options set by +// MockReleaseOptions. This function should typically not be used outside of +// testing. func ReleaseMock(opts *MockReleaseOptions) *release.Release { date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0} @@ -273,6 +327,11 @@ func ReleaseMock(opts *MockReleaseOptions) *release.Release { } } + config := opts.Config + if config == nil { + config = &chart.Config{Raw: `name: "value"`} + } + scode := release.Status_DEPLOYED if opts.StatusCode > 0 { scode = opts.StatusCode @@ -287,7 +346,7 @@ func ReleaseMock(opts *MockReleaseOptions) *release.Release { Description: description, }, Chart: ch, - Config: &chart.Config{Raw: `name: "value"`}, + Config: config, Version: version, Namespace: namespace, Hooks: []*release.Hook{ @@ -303,3 +362,39 @@ func ReleaseMock(opts *MockReleaseOptions) *release.Release { Manifest: MockManifest, } } + +// RenderReleaseMock will take a release (usually produced by helm.ReleaseMock) +// and will render the Manifest inside using the local mechanism (no tiller). +// (Compare to renderResources in pkg/tiller) +func RenderReleaseMock(r *release.Release, asUpgrade bool) error { + if r == nil || r.Chart == nil || r.Chart.Metadata == nil { + return errors.New("a release with a chart with metadata must be provided to render the manifests") + } + + renderOpts := renderutil.Options{ + ReleaseOptions: chartutil.ReleaseOptions{ + Name: r.Name, + Namespace: r.Namespace, + Time: r.Info.LastDeployed, + Revision: int(r.Version), + IsUpgrade: asUpgrade, + IsInstall: !asUpgrade, + }, + } + rendered, err := renderutil.Render(r.Chart, r.Config, renderOpts) + if err != nil { + return err + } + + b := bytes.NewBuffer(nil) + for _, m := range manifest.SplitManifests(rendered) { + // Remove empty manifests + if len(strings.TrimSpace(m.Content)) == 0 { + continue + } + b.WriteString("\n---\n# Source: " + m.Name + "\n") + b.WriteString(m.Content) + } + r.Manifest = b.String() + return nil +} diff --git a/pkg/helm/fake_test.go b/pkg/helm/fake_test.go index f16fbaf4c..ecb0a2855 100644 --- a/pkg/helm/fake_test.go +++ b/pkg/helm/fake_test.go @@ -17,6 +17,7 @@ limitations under the License. package helm import ( + "fmt" "reflect" "testing" @@ -25,6 +26,57 @@ import ( rls "k8s.io/helm/pkg/proto/hapi/services" ) +const cmInputTemplate = `kind: ConfigMap +apiVersion: v1 +metadata: + name: example +data: + Release: +{{.Release | toYaml | indent 4}} +` +const cmOutputTemplate = ` +--- +# Source: installChart/templates/cm.yaml +kind: ConfigMap +apiVersion: v1 +metadata: + name: example +data: + Release: + IsInstall: %t + IsUpgrade: %t + Name: new-release + Namespace: default + Revision: %d + Service: Tiller + Time: + seconds: 242085845 + +` + +var installChart *chart.Chart + +func init() { + installChart = &chart.Chart{ + Metadata: &chart.Metadata{Name: "installChart"}, + Templates: []*chart.Template{ + {Name: "templates/cm.yaml", Data: []byte(cmInputTemplate)}, + }, + } +} + +func releaseWithChart(opts *MockReleaseOptions) *release.Release { + if opts.Chart == nil { + opts.Chart = installChart + } + return ReleaseMock(opts) +} + +func withManifest(r *release.Release, isUpgrade bool) *release.Release { + r.Manifest = fmt.Sprintf(cmOutputTemplate, !isUpgrade, isUpgrade, r.Version) + return r +} + func TestFakeClient_ReleaseStatus(t *testing.T) { releasePresent := ReleaseMock(&MockReleaseOptions{Name: "release-present"}) releaseNotPresent := ReleaseMock(&MockReleaseOptions{Name: "release-not-present"}) @@ -117,9 +169,9 @@ func TestFakeClient_ReleaseStatus(t *testing.T) { } func TestFakeClient_InstallReleaseFromChart(t *testing.T) { - installChart := &chart.Chart{} type fields struct { - Rels []*release.Release + Rels []*release.Release + RenderManifests bool } type args struct { ns string @@ -143,10 +195,10 @@ func TestFakeClient_InstallReleaseFromChart(t *testing.T) { opts: []InstallOption{ReleaseName("new-release")}, }, want: &rls.InstallReleaseResponse{ - Release: ReleaseMock(&MockReleaseOptions{Name: "new-release"}), + Release: releaseWithChart(&MockReleaseOptions{Name: "new-release"}), }, relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), + releaseWithChart(&MockReleaseOptions{Name: "new-release"}), }, wantErr: false, }, @@ -160,10 +212,10 @@ func TestFakeClient_InstallReleaseFromChart(t *testing.T) { opts: []InstallOption{ReleaseName("new-release"), InstallDescription("foo-bar")}, }, want: &rls.InstallReleaseResponse{ - Release: ReleaseMock(&MockReleaseOptions{Name: "new-release", Description: "foo-bar"}), + Release: releaseWithChart(&MockReleaseOptions{Name: "new-release", Description: "foo-bar"}), }, relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release", Description: "foo-bar"}), + releaseWithChart(&MockReleaseOptions{Name: "new-release", Description: "foo-bar"}), }, wantErr: false, }, @@ -171,7 +223,7 @@ func TestFakeClient_InstallReleaseFromChart(t *testing.T) { name: "Try to add a release where the name already exists.", fields: fields{ Rels: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), + releaseWithChart(&MockReleaseOptions{Name: "new-release"}), }, }, args: args{ @@ -179,16 +231,35 @@ func TestFakeClient_InstallReleaseFromChart(t *testing.T) { opts: []InstallOption{ReleaseName("new-release")}, }, relsAfter: []*release.Release{ - ReleaseMock(&MockReleaseOptions{Name: "new-release"}), + releaseWithChart(&MockReleaseOptions{Name: "new-release"}), }, want: nil, wantErr: true, }, + { + name: "Render the given chart.", + fields: fields{ + Rels: []*release.Release{}, + RenderManifests: true, + }, + args: args{ + ns: "default", + opts: []InstallOption{ReleaseName("new-release")}, + }, + want: &rls.InstallReleaseResponse{ + Release: withManifest(releaseWithChart(&MockReleaseOptions{Name: "new-release"}), false), + }, + relsAfter: []*release.Release{ + withManifest(releaseWithChart(&MockReleaseOptions{Name: "new-release"}), false), + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &FakeClient{ - Rels: tt.fields.Rels, + Rels: tt.fields.Rels, + RenderManifests: tt.fields.RenderManifests, } got, err := c.InstallReleaseFromChart(installChart, tt.args.ns, tt.args.opts...) if (err != nil) != tt.wantErr { @@ -293,7 +364,84 @@ func TestFakeClient_DeleteRelease(t *testing.T) { } if !reflect.DeepEqual(c.Rels, tt.relsAfter) { - t.Errorf("FakeClient.InstallReleaseFromChart() rels = %v, expected %v", got, tt.relsAfter) + t.Errorf("FakeClient.InstallReleaseFromChart() rels = %v, expected %v", c.Rels, tt.relsAfter) + } + }) + } +} + +func TestFakeClient_UpdateReleaseFromChart(t *testing.T) { + type fields struct { + Rels []*release.Release + RenderManifests bool + } + type args struct { + release string + opts []UpdateOption + } + tests := []struct { + name string + fields fields + args args + want *rls.UpdateReleaseResponse + relsAfter []*release.Release + wantErr bool + }{ + { + name: "Update release.", + fields: fields{ + Rels: []*release.Release{ + releaseWithChart(&MockReleaseOptions{Name: "new-release"}), + }, + }, + args: args{ + release: "new-release", + opts: []UpdateOption{}, + }, + want: &rls.UpdateReleaseResponse{ + Release: releaseWithChart(&MockReleaseOptions{Name: "new-release", Version: 2}), + }, + relsAfter: []*release.Release{ + releaseWithChart(&MockReleaseOptions{Name: "new-release", Version: 2}), + }, + }, + { + name: "Update and render given chart.", + fields: fields{ + Rels: []*release.Release{ + releaseWithChart(&MockReleaseOptions{Name: "new-release"}), + }, + RenderManifests: true, + }, + args: args{ + release: "new-release", + opts: []UpdateOption{}, + }, + want: &rls.UpdateReleaseResponse{ + Release: withManifest(releaseWithChart(&MockReleaseOptions{Name: "new-release", Version: 2}), true), + }, + relsAfter: []*release.Release{ + withManifest(releaseWithChart(&MockReleaseOptions{Name: "new-release", Version: 2}), true), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &FakeClient{ + Rels: tt.fields.Rels, + RenderManifests: tt.fields.RenderManifests, + } + got, err := c.UpdateReleaseFromChart(tt.args.release, installChart, tt.args.opts...) + if (err != nil) != tt.wantErr { + t.Errorf("FakeClient.UpdateReleaseFromChart() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("FakeClient.UpdateReleaseFromChart() =\n%v\nwant\n%v", got, tt.want) + } + if !reflect.DeepEqual(c.Rels, tt.relsAfter) { + t.Errorf("FakeClient.UpdateReleaseFromChart() rels =\n%v\nwant\n%v", c.Rels, tt.relsAfter) } }) }