You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
helm/pkg/helm/fake.go

438 lines
12 KiB
Go

/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package helm // import "k8s.io/helm/pkg/helm"
import (
"bytes"
"errors"
"fmt"
"math/rand"
"strings"
"sync"
"github.com/golang/protobuf/ptypes/timestamp"
"golang.org/x/net/context"
"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"
storageerrors "k8s.io/helm/pkg/storage/errors"
)
// FakeClient implements Interface
type FakeClient struct {
Rels []*release.Release
Responses map[string]release.TestRun_Status
Opts options
RenderManifests bool
}
// Option returns the fake release client
func (c *FakeClient) Option(opts ...Option) Interface {
for _, opt := range opts {
opt(&c.Opts)
}
return c
}
var _ Interface = &FakeClient{}
var _ Interface = (*FakeClient)(nil)
// ListReleases lists the current releases
func (c *FakeClient) ListReleases(opts ...ReleaseListOption) (*rls.ListReleasesResponse, error) {
reqOpts := c.Opts
for _, opt := range opts {
opt(&reqOpts)
}
req := &reqOpts.listReq
rels := c.Rels
count := int64(len(c.Rels))
var next string
limit := req.GetLimit()
// TODO: Handle all other options.
if limit != 0 && limit < count {
rels = rels[:limit]
count = limit
next = c.Rels[limit].GetName()
}
resp := &rls.ListReleasesResponse{
Count: count,
Releases: rels,
}
if next != "" {
resp.Next = next
}
return resp, nil
}
// InstallRelease creates a new release and returns a InstallReleaseResponse containing that release
func (c *FakeClient) InstallRelease(chStr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) {
chart := &chart.Chart{}
return c.InstallReleaseFromChart(chart, ns, opts...)
}
// InstallReleaseWithContext creates a new release and returns a InstallReleaseResponse containing that release and accepts a context
func (c *FakeClient) InstallReleaseWithContext(ctx context.Context, chStr, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) {
return c.InstallRelease(chStr, ns, opts...)
}
// InstallReleaseFromChartWithContext adds a new MockRelease to the fake client and returns a InstallReleaseResponse containing that release and accepts a context
func (c *FakeClient) InstallReleaseFromChartWithContext(ctx context.Context, chart *chart.Chart, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) {
return c.InstallReleaseFromChart(chart, ns, opts...)
}
// InstallReleaseFromChart adds a new MockRelease to the fake client and returns a InstallReleaseResponse containing that release
func (c *FakeClient) InstallReleaseFromChart(chart *chart.Chart, ns string, opts ...InstallOption) (*rls.InstallReleaseResponse, error) {
for _, opt := range opts {
opt(&c.Opts)
}
releaseName := c.Opts.instReq.Name
releaseDescription := c.Opts.instReq.Description
// Check to see if the release already exists.
rel, err := c.ReleaseStatus(releaseName, nil)
if err == nil && rel != nil {
return nil, errors.New("cannot re-use a name that is still in use")
}
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)
}
return &rls.InstallReleaseResponse{
Release: release,
}, nil
}
// DeleteRelease deletes a release from the FakeClient
func (c *FakeClient) DeleteRelease(rlsName string, opts ...DeleteOption) (*rls.UninstallReleaseResponse, error) {
for i, rel := range c.Rels {
if rel.Name == rlsName {
c.Rels = append(c.Rels[:i], c.Rels[i+1:]...)
return &rls.UninstallReleaseResponse{
Release: rel,
}, nil
}
}
return nil, storageerrors.ErrReleaseNotFound(rlsName)
}
// GetVersion returns a fake version
func (c *FakeClient) GetVersion(opts ...VersionOption) (*rls.GetVersionResponse, error) {
return &rls.GetVersionResponse{
Version: &version.Version{
SemVer: "1.2.3-fakeclient+testonly",
},
}, nil
}
// UpdateRelease returns an UpdateReleaseResponse containing the updated release, if it exists
func (c *FakeClient) UpdateRelease(rlsName string, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) {
return c.UpdateReleaseFromChart(rlsName, &chart.Chart{}, opts...)
}
// UpdateReleaseWithContext returns an UpdateReleaseResponse containing the updated release, if it exists and accepts a context
func (c *FakeClient) UpdateReleaseWithContext(ctx context.Context, rlsName string, chStr string, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) {
return c.UpdateRelease(rlsName, chStr, opts...)
}
// UpdateReleaseFromChartWithContext returns an UpdateReleaseResponse containing the updated release, if it exists and accepts a context
func (c *FakeClient) UpdateReleaseFromChartWithContext(ctx context.Context, rlsName string, newChart *chart.Chart, opts ...UpdateOption) (*rls.UpdateReleaseResponse, error) {
return c.UpdateReleaseFromChart(rlsName, newChart, opts...)
}
// UpdateReleaseFromChart returns an UpdateReleaseResponse containing the updated release, if it exists
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
}
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
func (c *FakeClient) RollbackRelease(rlsName string, opts ...RollbackOption) (*rls.RollbackReleaseResponse, error) {
return nil, nil
}
// ReleaseStatus returns a release status response with info from the matching release name.
func (c *FakeClient) ReleaseStatus(rlsName string, opts ...StatusOption) (*rls.GetReleaseStatusResponse, error) {
for _, rel := range c.Rels {
if rel.Name == rlsName {
return &rls.GetReleaseStatusResponse{
Name: rel.Name,
Info: rel.Info,
Namespace: rel.Namespace,
}, nil
}
}
return nil, storageerrors.ErrReleaseNotFound(rlsName)
}
// ReleaseContent returns the configuration for the matching release name in the fake release client.
func (c *FakeClient) ReleaseContent(rlsName string, opts ...ContentOption) (resp *rls.GetReleaseContentResponse, err error) {
for _, rel := range c.Rels {
if rel.Name == rlsName {
return &rls.GetReleaseContentResponse{
Release: rel,
}, nil
}
}
return resp, storageerrors.ErrReleaseNotFound(rlsName)
}
// ReleaseHistory returns a release's revision history.
func (c *FakeClient) ReleaseHistory(rlsName string, opts ...HistoryOption) (*rls.GetHistoryResponse, error) {
reqOpts := c.Opts
for _, opt := range opts {
opt(&reqOpts)
}
maxLen := int(reqOpts.histReq.Max)
var resp rls.GetHistoryResponse
for _, rel := range c.Rels {
if maxLen > 0 && len(resp.Releases) >= maxLen {
return &resp, nil
}
if rel.Name == rlsName {
resp.Releases = append(resp.Releases, rel)
}
}
return &resp, nil
}
// RunReleaseTest executes a pre-defined tests on a release
func (c *FakeClient) RunReleaseTest(rlsName string, opts ...ReleaseTestOption) (<-chan *rls.TestReleaseResponse, <-chan error) {
results := make(chan *rls.TestReleaseResponse)
errc := make(chan error, 1)
go func() {
var wg sync.WaitGroup
for m, s := range c.Responses {
wg.Add(1)
go func(msg string, status release.TestRun_Status) {
defer wg.Done()
results <- &rls.TestReleaseResponse{Msg: msg, Status: status}
}(m, s)
}
wg.Wait()
close(results)
close(errc)
}()
return results, errc
}
// PingTiller pings the Tiller pod and ensures that it is up and running
func (c *FakeClient) PingTiller() error {
return nil
}
// MockHookTemplate is the hook template used for all mock release objects.
var MockHookTemplate = `apiVersion: v1
kind: Job
metadata:
annotations:
"helm.sh/hook": pre-install
`
// MockManifest is the manifest used for all mock release objects.
var MockManifest = `apiVersion: v1
kind: Secret
metadata:
name: fixture
`
// MockReleaseOptions allows for user-configurable options on mock release objects.
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.
func ReleaseMock(opts *MockReleaseOptions) *release.Release {
date := timestamp.Timestamp{Seconds: 242085845, Nanos: 0}
name := opts.Name
if name == "" {
name = fmt.Sprintf("testrelease-%d", rand.Intn(100))
}
var version int32 = 1
if opts.Version != 0 {
version = opts.Version
}
namespace := opts.Namespace
if namespace == "" {
namespace = "default"
}
description := opts.Description
if description == "" {
description = "Release mock"
}
ch := opts.Chart
if opts.Chart == nil {
ch = &chart.Chart{
Metadata: &chart.Metadata{
Name: "foo",
Version: "0.1.0-beta.1",
},
Templates: []*chart.Template{
{Name: "templates/foo.tpl", Data: []byte(MockManifest)},
},
}
}
config := opts.Config
if config == nil {
config = &chart.Config{Raw: `name: "value"`}
}
scode := release.Status_DEPLOYED
if opts.StatusCode > 0 {
scode = opts.StatusCode
}
return &release.Release{
Name: name,
Info: &release.Info{
FirstDeployed: &date,
LastDeployed: &date,
Status: &release.Status{Code: scode},
Description: description,
},
Chart: ch,
Config: config,
Version: version,
Namespace: namespace,
Hooks: []*release.Hook{
{
Name: "pre-install-hook",
Kind: "Job",
Path: "pre-install-hook.yaml",
Manifest: MockHookTemplate,
LastRun: &date,
Events: []release.Hook_Event{release.Hook_PRE_INSTALL},
},
},
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
}