Commit 774789c7 authored by Taylor Thomas's avatar Taylor Thomas

feat(*): Adds support for timeout flag

Installs, rollback, upgrade, and delete now accept a `--timeout` flag
that allows the user to specify the maximum number of seconds that
any kubernetes command can take.

Closes #1678
parent 5fc020f0
......@@ -190,6 +190,8 @@ message UpdateReleaseRequest {
bool disable_hooks = 5;
// Performs pods restart for resources if applicable
bool recreate = 6;
// timeout specifies the max amount of time any kubernetes client command can run.
int64 timeout = 7;
}
// UpdateReleaseResponse is the response to an update request.
......@@ -208,6 +210,8 @@ message RollbackReleaseRequest {
int32 version = 4;
// Performs pods restart for resources if applicable
bool recreate = 5;
// timeout specifies the max amount of time any kubernetes client command can run.
int64 timeout = 6;
}
// RollbackReleaseResponse is the response to an update request.
......@@ -239,6 +243,9 @@ message InstallReleaseRequest {
// ReuseName requests that Tiller re-uses a name, instead of erroring out.
bool reuse_name = 7;
// timeout specifies the max amount of time any kubernetes client command can run.
int64 timeout = 8;
}
// InstallReleaseResponse is the response from a release installation.
......@@ -254,6 +261,8 @@ message UninstallReleaseRequest {
bool disable_hooks = 2;
// Purge removes the release from the store and make its name free for later use.
bool purge = 3;
// timeout specifies the max amount of time any kubernetes client command can run.
int64 timeout = 4;
}
// UninstallReleaseResponse represents a successful response to an uninstall request.
......
......@@ -39,6 +39,7 @@ type deleteCmd struct {
dryRun bool
disableHooks bool
purge bool
timeout int64
out io.Writer
client helm.Interface
......@@ -77,6 +78,7 @@ func newDeleteCmd(c helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&del.dryRun, "dry-run", false, "simulate a delete")
f.BoolVar(&del.disableHooks, "no-hooks", false, "prevent hooks from running during deletion")
f.BoolVar(&del.purge, "purge", false, "remove the release from the store and make its name free for later use")
f.Int64Var(&del.timeout, "timeout", 300, "time in seconds to wait for any individual kubernetes operation (like Jobs for hooks)")
return cmd
}
......@@ -86,6 +88,7 @@ func (d *deleteCmd) run() error {
helm.DeleteDryRun(d.dryRun),
helm.DeleteDisableHooks(d.disableHooks),
helm.DeletePurge(d.purge),
helm.DeleteTimeout(d.timeout),
}
res, err := d.client.DeleteRelease(d.name, opts...)
if res != nil && res.Info != "" {
......
......@@ -33,6 +33,13 @@ func TestDelete(t *testing.T) {
expected: "", // Output of a delete is an empty string and exit 0.
resp: releaseMock(&releaseOptions{name: "aeneas"}),
},
{
name: "delete with timeout",
args: []string{"aeneas"},
flags: []string{"--timeout", "120"},
expected: "",
resp: releaseMock(&releaseOptions{name: "aeneas"}),
},
{
name: "delete without hooks",
args: []string{"aeneas"},
......
......@@ -104,6 +104,7 @@ type installCmd struct {
values string
nameTemplate string
version string
timeout int64
}
type valueFiles []string
......@@ -160,6 +161,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it")
f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification")
f.StringVar(&inst.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed")
f.Int64Var(&inst.timeout, "timeout", 300, "time in seconds to wait for any individual kubernetes operation (like Jobs for hooks)")
return cmd
}
......@@ -195,7 +197,8 @@ func (i *installCmd) run() error {
helm.ReleaseName(i.name),
helm.InstallDryRun(i.dryRun),
helm.InstallReuseName(i.replace),
helm.InstallDisableHooks(i.disableHooks))
helm.InstallDisableHooks(i.disableHooks),
helm.InstallTimeout(i.timeout))
if err != nil {
return prettyError(err)
}
......
......@@ -82,6 +82,14 @@ func TestInstall(t *testing.T) {
expected: "aeneas",
resp: releaseMock(&releaseOptions{name: "aeneas"}),
},
// Install, with timeout
{
name: "install with a timeout",
args: []string{"testdata/testcharts/alpine"},
flags: strings.Split("--timeout 120", " "),
expected: "foobar",
resp: releaseMock(&releaseOptions{name: "foobar"}),
},
// Install, using the name-template
{
name: "install with name-template",
......
......@@ -39,6 +39,7 @@ type rollbackCmd struct {
disableHooks bool
out io.Writer
client helm.Interface
timeout int64
}
func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command {
......@@ -74,6 +75,7 @@ func newRollbackCmd(c helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&rollback.dryRun, "dry-run", false, "simulate a rollback")
f.BoolVar(&rollback.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
f.BoolVar(&rollback.disableHooks, "no-hooks", false, "prevent hooks from running during rollback")
f.Int64Var(&rollback.timeout, "timeout", 300, "time in seconds to wait for any individual kubernetes operation (like Jobs for hooks)")
return cmd
}
......@@ -85,7 +87,7 @@ func (r *rollbackCmd) run() error {
helm.RollbackRecreate(r.recreate),
helm.RollbackDisableHooks(r.disableHooks),
helm.RollbackVersion(r.revision),
)
helm.RollbackTimeout(r.timeout))
if err != nil {
return prettyError(err)
}
......
......@@ -31,6 +31,12 @@ func TestRollbackCmd(t *testing.T) {
args: []string{"funny-honey", "1"},
expected: "Rollback was a success! Happy Helming!",
},
{
name: "rollback a release with timeout",
args: []string{"funny-honey", "1"},
flags: []string{"--timeout", "120"},
expected: "Rollback was a success! Happy Helming!",
},
{
name: "rollback a release without revision",
args: []string{"funny-honey"},
......
......@@ -63,6 +63,7 @@ type upgradeCmd struct {
install bool
namespace string
version string
timeout int64
}
func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
......@@ -102,6 +103,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command {
f.BoolVarP(&upgrade.install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
f.StringVar(&upgrade.namespace, "namespace", "default", "namespace to install the release into (only used if --install is set)")
f.StringVar(&upgrade.version, "version", "", "specify the exact chart version to use. If this is not specified, the latest version is used")
f.Int64Var(&upgrade.timeout, "timeout", 300, "time in seconds to wait for any individual kubernetes operation (like Jobs for hooks)")
f.MarkDeprecated("disable-hooks", "use --no-hooks instead")
......@@ -136,6 +138,7 @@ func (u *upgradeCmd) run() error {
keyring: u.keyring,
values: u.values,
namespace: u.namespace,
timeout: u.timeout,
}
return ic.run()
}
......@@ -152,7 +155,8 @@ func (u *upgradeCmd) run() error {
helm.UpdateValueOverrides(rawVals),
helm.UpgradeDryRun(u.dryRun),
helm.UpgradeRecreate(u.recreate),
helm.UpgradeDisableHooks(u.disableHooks))
helm.UpgradeDisableHooks(u.disableHooks),
helm.UpgradeTimeout(u.timeout))
if err != nil {
return fmt.Errorf("UPGRADE FAILED: %v", prettyError(err))
}
......
......@@ -62,6 +62,23 @@ func TestUpgradeCmd(t *testing.T) {
t.Errorf("Error loading updated chart: %v", err)
}
// update chart version again
cfile = &chart.Metadata{
Name: "testUpgradeChart",
Description: "A Helm chart for Kubernetes",
Version: "0.1.3",
}
chartPath, err = chartutil.Create(cfile, tmpChart)
if err != nil {
t.Errorf("Error creating chart: %v", err)
}
var ch2 *chart.Chart
ch2, err = chartutil.Load(chartPath)
if err != nil {
t.Errorf("Error loading updated chart: %v", err)
}
tests := []releaseCase{
{
name: "upgrade a release",
......@@ -69,6 +86,13 @@ func TestUpgradeCmd(t *testing.T) {
resp: releaseMock(&releaseOptions{name: "funny-bunny", version: 2, chart: ch}),
expected: "funny-bunny has been upgraded. Happy Helming!\n",
},
{
name: "upgrade a release with timeout",
args: []string{"funny-bunny", chartPath},
flags: []string{"--timeout", "120"},
resp: releaseMock(&releaseOptions{name: "funny-bunny", version: 3, chart: ch2}),
expected: "funny-bunny has been upgraded. Happy Helming!\n",
},
{
name: "install a release with 'upgrade --install'",
args: []string{"zany-bunny", chartPath},
......@@ -76,6 +100,13 @@ func TestUpgradeCmd(t *testing.T) {
resp: releaseMock(&releaseOptions{name: "zany-bunny", version: 1, chart: ch}),
expected: "zany-bunny has been upgraded. Happy Helming!\n",
},
{
name: "install a release with 'upgrade --install' and timeout",
args: []string{"crazy-bunny", chartPath},
flags: []string{"-i", "--timeout", "120"},
resp: releaseMock(&releaseOptions{name: "crazy-bunny", version: 1, chart: ch}),
expected: "crazy-bunny has been upgraded. Happy Helming!\n",
},
}
cmd := func(c *fakeReleaseClient, out io.Writer) *cobra.Command {
......
......@@ -151,6 +151,34 @@ func ReleaseName(name string) InstallOption {
}
}
// InstallTimeout specifies the number of seconds before kubernetes calls timeout
func InstallTimeout(timeout int64) InstallOption {
return func(opts *options) {
opts.instReq.Timeout = timeout
}
}
// UpgradeTimeout specifies the number of seconds before kubernetes calls timeout
func UpgradeTimeout(timeout int64) UpdateOption {
return func(opts *options) {
opts.updateReq.Timeout = timeout
}
}
// DeleteTimeout specifies the number of seconds before kubernetes calls timeout
func DeleteTimeout(timeout int64) DeleteOption {
return func(opts *options) {
opts.uninstallReq.Timeout = timeout
}
}
// RollbackTimeout specifies the number of seconds before kubernetes calls timeout
func RollbackTimeout(timeout int64) RollbackOption {
return func(opts *options) {
opts.rollbackReq.Timeout = timeout
}
}
// UpdateValueOverrides specifies a list of values to include when upgrading
func UpdateValueOverrides(raw []byte) UpdateOption {
return func(opts *options) {
......
......@@ -258,6 +258,12 @@ func skipIfNotFound(err error) error {
return err
}
func watchTimeout(t time.Duration) ResourceActorFunc {
return func(info *resource.Info) error {
return watchUntilReady(t, info)
}
}
// WatchUntilReady watches the resource given in the reader, and waits until it is ready.
//
// This function is mainly for hook implementations. It watches for a resource to
......@@ -270,10 +276,10 @@ func skipIfNotFound(err error) error {
// ascertained by watching the Status fields in a job's output.
//
// Handling for other kinds will be added as necessary.
func (c *Client) WatchUntilReady(namespace string, reader io.Reader) error {
func (c *Client) WatchUntilReady(namespace string, reader io.Reader, timeout int64) error {
// For jobs, there's also the option to do poll c.Jobs(namespace).Get():
// https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300
return perform(c, namespace, reader, watchUntilReady)
return perform(c, namespace, reader, watchTimeout(time.Duration(timeout)*time.Second))
}
func perform(c *Client, namespace string, reader io.Reader, fn ResourceActorFunc) error {
......@@ -382,15 +388,14 @@ func recreatePods(client *internalclientset.Clientset, namespace string, selecto
return nil
}
func watchUntilReady(info *resource.Info) error {
func watchUntilReady(timeout time.Duration, info *resource.Info) error {
w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion)
if err != nil {
return err
}
kind := info.Mapping.GroupVersionKind.Kind
log.Printf("Watching for changes to %s %s", kind, info.Name)
timeout := time.Minute * 5
log.Printf("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout)
// What we watch for depends on the Kind.
// - For a Job, we watch for completion.
......
This diff is collapsed.
......@@ -122,7 +122,7 @@ type KubeClient interface {
// For Jobs, "ready" means the job ran to completion (excited without error).
// For all other kinds, it means the kind was created or modified without
// error.
WatchUntilReady(namespace string, reader io.Reader) error
WatchUntilReady(namespace string, reader io.Reader, timeout int64) error
// Update updates one or more resources or creates the resource
// if it doesn't exist
......@@ -161,7 +161,7 @@ func (p *PrintingKubeClient) Delete(ns string, r io.Reader) error {
}
// WatchUntilReady implements KubeClient WatchUntilReady.
func (p *PrintingKubeClient) WatchUntilReady(ns string, r io.Reader) error {
func (p *PrintingKubeClient) WatchUntilReady(ns string, r io.Reader, t int64) error {
_, err := io.Copy(p.Out, r)
return err
}
......
......@@ -47,7 +47,7 @@ func (k *mockKubeClient) Delete(ns string, r io.Reader) error {
func (k *mockKubeClient) Update(ns string, currentReader, modifiedReader io.Reader, recreate bool) error {
return nil
}
func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader) error {
func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader, t int64) error {
return nil
}
......
......@@ -333,9 +333,9 @@ func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.R
return res, nil
}
// pre-ugrade hooks
// pre-upgrade hooks
if !req.DisableHooks {
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, preUpgrade); err != nil {
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, preUpgrade, req.Timeout); err != nil {
return res, err
}
}
......@@ -351,7 +351,7 @@ func (s *ReleaseServer) performUpdate(originalRelease, updatedRelease *release.R
// post-upgrade hooks
if !req.DisableHooks {
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, postUpgrade); err != nil {
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, postUpgrade, req.Timeout); err != nil {
return res, err
}
}
......@@ -473,7 +473,7 @@ func (s *ReleaseServer) performRollback(currentRelease, targetRelease *release.R
// pre-rollback hooks
if !req.DisableHooks {
if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, preRollback); err != nil {
if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, preRollback, req.Timeout); err != nil {
return res, err
}
}
......@@ -489,7 +489,7 @@ func (s *ReleaseServer) performRollback(currentRelease, targetRelease *release.R
// post-rollback hooks
if !req.DisableHooks {
if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, postRollback); err != nil {
if err := s.execHook(targetRelease.Hooks, targetRelease.Name, targetRelease.Namespace, postRollback, req.Timeout); err != nil {
return res, err
}
}
......@@ -809,7 +809,7 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install
// pre-install hooks
if !req.DisableHooks {
if err := s.execHook(r.Hooks, r.Name, r.Namespace, preInstall); err != nil {
if err := s.execHook(r.Hooks, r.Name, r.Namespace, preInstall, req.Timeout); err != nil {
return res, err
}
}
......@@ -854,7 +854,7 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install
// post-install hooks
if !req.DisableHooks {
if err := s.execHook(r.Hooks, r.Name, r.Namespace, postInstall); err != nil {
if err := s.execHook(r.Hooks, r.Name, r.Namespace, postInstall, req.Timeout); err != nil {
log.Printf("warning: Release %q failed post-install: %s", r.Name, err)
r.Info.Status.Code = release.Status_FAILED
s.recordRelease(r, false)
......@@ -875,7 +875,7 @@ func (s *ReleaseServer) performRelease(r *release.Release, req *services.Install
return res, nil
}
func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string) error {
func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error {
kubeCli := s.env.KubeClient
code, ok := events[hook]
if !ok {
......@@ -903,7 +903,7 @@ func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook strin
// No way to rewind a bytes.Buffer()?
b.Reset()
b.WriteString(h.Manifest)
if err := kubeCli.WatchUntilReady(namespace, b); err != nil {
if err := kubeCli.WatchUntilReady(namespace, b, timeout); err != nil {
log.Printf("warning: Release %q pre-install %s could not complete: %s", name, h.Path, err)
return err
}
......@@ -964,7 +964,7 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
res := &services.UninstallReleaseResponse{Release: rel}
if !req.DisableHooks {
if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, preDelete); err != nil {
if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, preDelete, req.Timeout); err != nil {
return res, err
}
}
......@@ -1010,7 +1010,7 @@ func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR
}
if !req.DisableHooks {
if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, postDelete); err != nil {
if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, postDelete, req.Timeout); err != nil {
es = append(es, err.Error())
}
}
......
......@@ -1375,7 +1375,7 @@ type hookFailingKubeClient struct {
environment.PrintingKubeClient
}
func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader) error {
func (h *hookFailingKubeClient) WatchUntilReady(ns string, r io.Reader, t int64) error {
return errors.New("Failed watch")
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment