Skip to content

Commit

Permalink
Create buildkite-agent step cancel --step "key" subcommand
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchbne committed Nov 8, 2024
1 parent 18f3148 commit 9799810
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 1 deletion.
1 change: 1 addition & 0 deletions agent/api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions api/steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,29 @@ func (c *Client) StepUpdate(ctx context.Context, stepIdOrKey string, stepUpdate

return c.doRequest(req, nil)
}

type StepCancel struct {
Force bool `json:"force,omitempty"`
}

type StepCancelResponse struct {
UUID string `json:"uuid"`
}

// StepCancel cancels a step
func (c *Client) StepCancel(ctx context.Context, stepIdOrKey string, stepCancel *StepCancel) (*StepCancelResponse, *Response, error) {
u := fmt.Sprintf("steps/%s/cancel", railsPathEscape(stepIdOrKey))

req, err := c.newRequest(ctx, "POST", u, stepCancel)
if err != nil {
return nil, nil, err
}

stepCancelResponse := new(StepCancelResponse)
resp, err := c.doRequest(req, stepCancelResponse)
if err != nil {
return nil, resp, err
}

return stepCancelResponse, resp, nil
}
3 changes: 2 additions & 1 deletion clicommand/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,11 @@ var BuildkiteAgentCommands = []cli.Command{
},
{
Name: "step",
Usage: "Get or update an attribute of a build step",
Usage: "Get or update an attribute of a build step, or cancel unfinished jobs for a step",
Subcommands: []cli.Command{
StepGetCommand,
StepUpdateCommand,
StepCancelCommand,
},
},
{
Expand Down
1 change: 1 addition & 0 deletions clicommand/config_completeness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var commandConfigPairs = []configCommandPair{
{Config: PipelineUploadConfig{}, Command: PipelineUploadCommand},
{Config: RedactorAddConfig{}, Command: RedactorAddCommand},
{Config: SecretGetConfig{}, Command: SecretGetCommand},
{Config: StepCancelConfig{}, Command: StepCancelCommand},
{Config: StepGetConfig{}, Command: StepGetCommand},
{Config: StepUpdateConfig{}, Command: StepUpdateCommand},
{Config: ToolKeygenConfig{}, Command: ToolKeygenCommand},
Expand Down
113 changes: 113 additions & 0 deletions clicommand/step_cancel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package clicommand

import (
"context"
"fmt"
"time"

"github.com/buildkite/agent/v3/api"
"github.com/buildkite/agent/v3/logger"
"github.com/buildkite/roko"
"github.com/urfave/cli"
)

const stepCancelHelpDescription = `Usage:
buildkite-agent step cancel <attribute> <value> [options...]
Description:
Cancel all unfinished jobs for a step
Example:
$ buildkite-agent step cancel --step "key"
$ buildkite-agent step cancel --step "key" --force`

type StepCancelConfig struct {
StepOrKey string `cli:"step" validate:"required"`
Force bool `cli:"force"`

// Global flags
Debug bool `cli:"debug"`
LogLevel string `cli:"log-level"`
NoColor bool `cli:"no-color"`
Experiments []string `cli:"experiment" normalize:"list"`
Profile string `cli:"profile"`

// API config
DebugHTTP bool `cli:"debug-http"`
AgentAccessToken string `cli:"agent-access-token" validate:"required"`
Endpoint string `cli:"endpoint" validate:"required"`
NoHTTP2 bool `cli:"no-http2"`
}

var StepCancelCommand = cli.Command{
Name: "cancel",
Usage: "Cancel all unfinished jobs for a step",
Description: stepCancelHelpDescription,
Flags: []cli.Flag{
cli.StringFlag{
Name: "step",
Value: "",
Usage: "The step to cancel. Can be either its ID (BUILDKITE_STEP_ID) or key (BUILDKITE_STEP_KEY)",
EnvVar: "BUILDKITE_STEP_ID",
},
cli.BoolFlag{
Name: "force",
Usage: "Don't wait for the agent to finish before cancelling the jobs",
EnvVar: "BUILDKITE_STEP_CANCEL_FORCE",
},

// API Flags
AgentAccessTokenFlag,
EndpointFlag,
NoHTTP2Flag,
DebugHTTPFlag,

// Global flags
NoColorFlag,
DebugFlag,
LogLevelFlag,
ExperimentsFlag,
ProfileFlag,
},
Action: func(c *cli.Context) error {
ctx, cfg, l, _, done := setupLoggerAndConfig[StepCancelConfig](context.Background(), c)
defer done()

return cancelStep(ctx, cfg, l)
},
}

func cancelStep(ctx context.Context, cfg StepCancelConfig, l logger.Logger) error {
// Create the API client
client := api.NewClient(l, loadAPIClientConfig(cfg, "AgentAccessToken"))

// Create the value to cancel
cancel := &api.StepCancel{
Force: cfg.Force,
}

// Post the change
if err := roko.NewRetrier(
roko.WithMaxAttempts(10),
roko.WithStrategy(roko.Constant(5*time.Second)),
).DoWithContext(ctx, func(r *roko.Retrier) error {
stepCancelResponse, resp, err := client.StepCancel(ctx, cfg.StepOrKey, cancel)
if resp != nil && (resp.StatusCode == 400 || resp.StatusCode == 401 || resp.StatusCode == 404) {
r.Break()
}
if err != nil {
l.Warn("%s (%s)", err, r)
return err
}

l.Info("Successfully cancelled step: %s", stepCancelResponse.UUID)
return nil
}); err != nil {
return fmt.Errorf("Failed to cancel step: %w", err)
}

return nil
}
52 changes: 52 additions & 0 deletions clicommand/step_cancel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package clicommand

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/buildkite/agent/v3/logger"
"github.com/stretchr/testify/assert"
)

func TestStepCancel(t *testing.T) {
t.Parallel()
ctx := context.Background()

t.Run("success", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(`{"uuid": "b0db1550-e68c-428f-9b4d-edf5599b2cff"}`))
}))

cfg := StepCancelConfig{
Force: true,
StepOrKey: "some-random-key",
AgentAccessToken: "agentaccesstoken",
Endpoint: server.URL,
}

l := logger.NewBuffer()
err := cancelStep(ctx, cfg, l)
assert.Nil(t, err)
assert.Contains(t, l.Messages, "[info] Successfully cancelled step: b0db1550-e68c-428f-9b4d-edf5599b2cff")
})

t.Run("failed", func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.WriteHeader(http.StatusInternalServerError)
}))

cfg := StepCancelConfig{
Force: true,
StepOrKey: "some-random-key",
AgentAccessToken: "agentaccesstoken",
Endpoint: server.URL,
}

l := logger.NewBuffer()
err := cancelStep(ctx, cfg, l)
assert.Contains(t, err.Error(), "Failed to cancel step")
})
}

0 comments on commit 9799810

Please sign in to comment.