From c0974fe9af78fc81ade2d7adffed3c858ce122df Mon Sep 17 00:00:00 2001 From: Tim Gross Date: Mon, 15 Jun 2020 15:01:54 -0400 Subject: [PATCH] multiregion CLI: nomad deployment unblock --- api/deployments.go | 19 +++ command/agent/deployment_endpoint.go | 28 ++++ command/commands.go | 5 + command/deployment_unblock.go | 131 ++++++++++++++++++ command/deployment_unblock_test.go | 63 +++++++++ .../hashicorp/nomad/api/deployments.go | 19 +++ 6 files changed, 265 insertions(+) create mode 100644 command/deployment_unblock.go create mode 100644 command/deployment_unblock_test.go diff --git a/api/deployments.go b/api/deployments.go index af8487782..4a4844246 100644 --- a/api/deployments.go +++ b/api/deployments.go @@ -107,6 +107,19 @@ func (d *Deployments) PromoteGroups(deploymentID string, groups []string, q *Wri return &resp, wm, nil } +// Unblock is used to unblock the given deployment. +func (d *Deployments) Unblock(deploymentID string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) { + var resp DeploymentUpdateResponse + req := &DeploymentUnblockRequest{ + DeploymentID: deploymentID, + } + wm, err := d.client.write("/v1/deployment/unblock/"+deploymentID, req, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, wm, nil +} + // SetAllocHealth is used to set allocation health for allocs that are part of // the given deployment func (d *Deployments) SetAllocHealth(deploymentID string, healthy, unhealthy []string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) { @@ -260,6 +273,12 @@ type DeploymentFailRequest struct { WriteRequest } +// DeploymentUnblockRequest is used to unblock a particular deployment +type DeploymentUnblockRequest struct { + DeploymentID string + WriteRequest +} + // SingleDeploymentResponse is used to respond with a single deployment type SingleDeploymentResponse struct { Deployment *Deployment diff --git a/command/agent/deployment_endpoint.go b/command/agent/deployment_endpoint.go index dd18a6564..7e923d733 100644 --- a/command/agent/deployment_endpoint.go +++ b/command/agent/deployment_endpoint.go @@ -47,6 +47,9 @@ func (s *HTTPServer) DeploymentSpecificRequest(resp http.ResponseWriter, req *ht case strings.HasPrefix(path, "allocation-health/"): deploymentID := strings.TrimPrefix(path, "allocation-health/") return s.deploymentSetAllocHealth(resp, req, deploymentID) + case strings.HasPrefix(path, "unblock/"): + deploymentID := strings.TrimPrefix(path, "unblock/") + return s.deploymentUnblock(resp, req, deploymentID) default: return s.deploymentQuery(resp, req, path) } @@ -120,6 +123,31 @@ func (s *HTTPServer) deploymentPromote(resp http.ResponseWriter, req *http.Reque return out, nil } +func (s *HTTPServer) deploymentUnblock(resp http.ResponseWriter, req *http.Request, deploymentID string) (interface{}, error) { + if req.Method != "PUT" && req.Method != "POST" { + return nil, CodedError(405, ErrInvalidMethod) + } + + var unblockRequest structs.DeploymentUnblockRequest + if err := decodeBody(req, &unblockRequest); err != nil { + return nil, CodedError(400, err.Error()) + } + if unblockRequest.DeploymentID == "" { + return nil, CodedError(400, "DeploymentID must be specified") + } + if unblockRequest.DeploymentID != deploymentID { + return nil, CodedError(400, "Deployment ID does not match") + } + s.parseWriteRequest(req, &unblockRequest.WriteRequest) + + var out structs.DeploymentUpdateResponse + if err := s.agent.RPC("Deployment.Unblock", &unblockRequest, &out); err != nil { + return nil, err + } + setIndex(resp, out.Index) + return out, nil +} + func (s *HTTPServer) deploymentSetAllocHealth(resp http.ResponseWriter, req *http.Request, deploymentID string) (interface{}, error) { if req.Method != "PUT" && req.Method != "POST" { return nil, CodedError(405, ErrInvalidMethod) diff --git a/command/commands.go b/command/commands.go index 7aabefe7f..6d9d60ea1 100644 --- a/command/commands.go +++ b/command/commands.go @@ -236,6 +236,11 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory { Meta: meta, }, nil }, + "deployment unblock": func() (cli.Command, error) { + return &DeploymentUnblockCommand{ + Meta: meta, + }, nil + }, "eval": func() (cli.Command, error) { return &EvalCommand{ Meta: meta, diff --git a/command/deployment_unblock.go b/command/deployment_unblock.go new file mode 100644 index 000000000..d530fc6c3 --- /dev/null +++ b/command/deployment_unblock.go @@ -0,0 +1,131 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/nomad/api/contexts" + "github.com/posener/complete" +) + +type DeploymentUnblockCommand struct { + Meta +} + +func (c *DeploymentUnblockCommand) Help() string { + helpText := ` +Usage: nomad deployment unblock [options] + + Unblock is used to unblock a multiregion deployment that's waiting for + peer region deployments to complete. + +General Options: + + ` + generalOptionsUsage() + ` + +Unblock Options: + + -detach + Return immediately instead of entering monitor mode. After deployment + unblock, the evaluation ID will be printed to the screen, which can be used + to examine the evaluation using the eval-status command. + + -verbose + Display full information. +` + return strings.TrimSpace(helpText) +} + +func (c *DeploymentUnblockCommand) Synopsis() string { + return "Unblock a blocked deployment" +} + +func (c *DeploymentUnblockCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-detach": complete.PredictNothing, + "-verbose": complete.PredictNothing, + }) +} + +func (c *DeploymentUnblockCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictFunc(func(a complete.Args) []string { + client, err := c.Meta.Client() + if err != nil { + return nil + } + + resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Deployments, nil) + if err != nil { + return []string{} + } + return resp.Matches[contexts.Deployments] + }) +} + +func (c *DeploymentUnblockCommand) Name() string { return "deployment unblock" } +func (c *DeploymentUnblockCommand) Run(args []string) int { + var detach, verbose bool + + flags := c.Meta.FlagSet(c.Name(), FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } + flags.BoolVar(&detach, "detach", false, "") + flags.BoolVar(&verbose, "verbose", false, "") + + if err := flags.Parse(args); err != nil { + return 1 + } + + // Check that we got exactly one argument + args = flags.Args() + if l := len(args); l != 1 { + c.Ui.Error("This command takes one argument: ") + c.Ui.Error(commandErrorText(c)) + return 1 + } + + dID := args[0] + + // Truncate the id unless full length is requested + length := shortId + if verbose { + length = fullId + } + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Do a prefix lookup + deploy, possible, err := getDeployment(client.Deployments(), dID) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error retrieving deployment: %s", err)) + return 1 + } + + if len(possible) != 0 { + c.Ui.Error(fmt.Sprintf("Prefix matched multiple deployments\n\n%s", formatDeployments(possible, length))) + return 1 + } + + u, _, err := client.Deployments().Unblock(deploy.ID, nil) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error unblocking deployment: %s", err)) + return 1 + } + + c.Ui.Output(fmt.Sprintf("Deployment %q unblocked", deploy.ID)) + evalCreated := u.EvalID != "" + + // Nothing to do + if detach || !evalCreated { + return 0 + } + + c.Ui.Output("") + mon := newMonitor(c.Ui, client, length) + return mon.monitor(u.EvalID, false) +} diff --git a/command/deployment_unblock_test.go b/command/deployment_unblock_test.go new file mode 100644 index 000000000..72adc8be9 --- /dev/null +++ b/command/deployment_unblock_test.go @@ -0,0 +1,63 @@ +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/nomad/nomad/mock" + "github.com/mitchellh/cli" + "github.com/posener/complete" + "github.com/stretchr/testify/assert" +) + +func TestDeploymentUnblockCommand_Implements(t *testing.T) { + t.Parallel() + var _ cli.Command = &DeploymentUnblockCommand{} +} + +func TestDeploymentUnblockCommand_Fails(t *testing.T) { + t.Parallel() + ui := new(cli.MockUi) + cmd := &DeploymentUnblockCommand{Meta: Meta{Ui: ui}} + + // Unblocks on misuse + if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 { + t.Fatalf("expected exit code 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, commandErrorText(cmd)) { + t.Fatalf("expected help output, got: %s", out) + } + ui.ErrorWriter.Reset() + + if code := cmd.Run([]string{"-address=nope", "12"}); code != 1 { + t.Fatalf("expected exit code 1, got: %d", code) + } + if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error retrieving deployment") { + t.Fatalf("expected unblocked query error, got: %s", out) + } + ui.ErrorWriter.Reset() +} + +func TestDeploymentUnblockCommand_AutocompleteArgs(t *testing.T) { + assert := assert.New(t) + t.Parallel() + + srv, _, url := testServer(t, true, nil) + defer srv.Shutdown() + + ui := new(cli.MockUi) + cmd := &DeploymentUnblockCommand{Meta: Meta{Ui: ui, flagAddress: url}} + + // Create a fake deployment + state := srv.Agent.Server().State() + d := mock.Deployment() + assert.Nil(state.UpsertDeployment(1000, d)) + + prefix := d.ID[:5] + args := complete.Args{Last: prefix} + predictor := cmd.AutocompleteArgs() + + res := predictor.Predict(args) + assert.Equal(1, len(res)) + assert.Equal(d.ID, res[0]) +} diff --git a/vendor/github.com/hashicorp/nomad/api/deployments.go b/vendor/github.com/hashicorp/nomad/api/deployments.go index af8487782..4a4844246 100644 --- a/vendor/github.com/hashicorp/nomad/api/deployments.go +++ b/vendor/github.com/hashicorp/nomad/api/deployments.go @@ -107,6 +107,19 @@ func (d *Deployments) PromoteGroups(deploymentID string, groups []string, q *Wri return &resp, wm, nil } +// Unblock is used to unblock the given deployment. +func (d *Deployments) Unblock(deploymentID string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) { + var resp DeploymentUpdateResponse + req := &DeploymentUnblockRequest{ + DeploymentID: deploymentID, + } + wm, err := d.client.write("/v1/deployment/unblock/"+deploymentID, req, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, wm, nil +} + // SetAllocHealth is used to set allocation health for allocs that are part of // the given deployment func (d *Deployments) SetAllocHealth(deploymentID string, healthy, unhealthy []string, q *WriteOptions) (*DeploymentUpdateResponse, *WriteMeta, error) { @@ -260,6 +273,12 @@ type DeploymentFailRequest struct { WriteRequest } +// DeploymentUnblockRequest is used to unblock a particular deployment +type DeploymentUnblockRequest struct { + DeploymentID string + WriteRequest +} + // SingleDeploymentResponse is used to respond with a single deployment type SingleDeploymentResponse struct { Deployment *Deployment