Skip to content

Commit

Permalink
Allow force deleting agent pools (#435)
Browse files Browse the repository at this point in the history
Sometimes, a user may want to force delete an agent pool, even if it is
associated with existing deployment settings. This is already supported
in the API, bringing the functionality to PSP.
  • Loading branch information
komalali authored Nov 6, 2024
1 parent 476f4b1 commit 3445009
Show file tree
Hide file tree
Showing 25 changed files with 281 additions and 66 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
### Improvements

- Allow force deleting agent pools. [#435](https://github.com/pulumi/pulumi-pulumiservice/pull/435)

### Bug Fixes

### Miscellaneous
2 changes: 1 addition & 1 deletion examples/ts-webhooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const stackWebhook = new service.Webhook("stack-webhook", {
organizationName: serviceOrg,
projectName: pulumi.getProject(),
stackName: pulumi.getStack(),
payloadUrl: "https://example.com",
payloadUrl: "https://hooks.slack.com/blahblah",
format: WebhookFormat.Slack,
groups: [ WebhookGroup.Stacks ],
filters: [WebhookFilters.DeploymentStarted, WebhookFilters.DeploymentSucceeded],
Expand Down
8 changes: 8 additions & 0 deletions provider/cmd/pulumi-resource-pulumiservice/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,10 @@
"description": "The agent pool's token's value.",
"type": "string",
"secret": true
},
"forceDestroy": {
"description": "Optional. Flag indicating whether to delete the agent pool even if stacks are configured to use it.",
"type": "boolean"
}
},
"required": [
Expand All @@ -682,6 +686,10 @@
"organizationName": {
"description": "The organization's name.",
"type": "string"
},
"forceDestroy": {
"description": "Optional. Flag indicating whether to delete the agent pool even if stacks are configured to use it.",
"type": "boolean"
}
},
"requiredInputs": [
Expand Down
6 changes: 3 additions & 3 deletions provider/pkg/internal/pulumiapi/accesstokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestDeleteAccessToken(t *testing.T) {
ExpectedReqMethod: http.MethodDelete,
ExpectedReqPath: "/api/user/tokens/" + tokenId,
ResponseCode: 404,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 404,
Message: "token not found",
},
Expand Down Expand Up @@ -75,7 +75,7 @@ func TestCreateAccessToken(t *testing.T) {
Description: desc,
},
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 401,
Message: "unauthorized",
},
Expand Down Expand Up @@ -131,7 +131,7 @@ func TestGetAccessToken(t *testing.T) {
ExpectedReqPath: "/api/user/tokens",
ExpectedReqBody: nil,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 401,
Message: "unauthorized",
},
Expand Down
14 changes: 10 additions & 4 deletions provider/pkg/internal/pulumiapi/agent_pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"path"
)

type AgentPoolClient interface {
CreateAgentPool(ctx context.Context, orgName, name, description string) (*AgentPool, error)
UpdateAgentPool(ctx context.Context, agentPoolId, orgName, name, description string) error
DeleteAgentPool(ctx context.Context, agentPoolId, orgName string) error
DeleteAgentPool(ctx context.Context, agentPoolId, orgName string, forceDestroy bool) error
GetAgentPool(ctx context.Context, agentPoolId, orgName string) (*AgentPool, error)
}

Expand Down Expand Up @@ -106,18 +107,23 @@ func (c *Client) UpdateAgentPool(ctx context.Context, agentPoolId, orgName, name
return nil
}

func (c *Client) DeleteAgentPool(ctx context.Context, agentPoolId, orgName string) error {
func (c *Client) DeleteAgentPool(ctx context.Context, agentPoolId, orgName string, forceDestroy bool) error {
if len(agentPoolId) == 0 {
return errors.New("agentPoolId length must be greater than zero")
}

if len(orgName) == 0 {
return errors.New("orgname length must be greater than zero")
return errors.New("orgName length must be greater than zero")
}

apiPath := path.Join("orgs", orgName, "agent-pools", agentPoolId)

_, err := c.do(ctx, http.MethodDelete, apiPath, nil, nil)
var err error
if forceDestroy {
_, err = c.doWithQuery(ctx, http.MethodDelete, apiPath, url.Values{"force": []string{"true"}}, nil, nil)
} else {
_, err = c.do(ctx, http.MethodDelete, apiPath, nil, nil)
}
if err != nil {
return fmt.Errorf("failed to delete agent pool %q: %w", agentPoolId, err)
}
Expand Down
10 changes: 5 additions & 5 deletions provider/pkg/internal/pulumiapi/agent_pools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,22 @@ func TestDeleteAgentPool(t *testing.T) {
ResponseCode: 204,
})
defer cleanup()
assert.NoError(t, c.DeleteAgentPool(teamCtx, agentPoolId, orgName))
assert.NoError(t, c.DeleteAgentPool(teamCtx, agentPoolId, orgName, false))
})

t.Run("Error", func(t *testing.T) {
c, cleanup := startTestServer(t, testServerConfig{
ExpectedReqMethod: http.MethodDelete,
ExpectedReqPath: "/api/orgs/anOrg/agent-pools/" + agentPoolId,
ResponseCode: 404,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 404,
Message: "agent pool not found",
},
})
defer cleanup()
assert.EqualError(t,
c.DeleteAgentPool(teamCtx, agentPoolId, orgName),
c.DeleteAgentPool(teamCtx, agentPoolId, orgName, false),
`failed to delete agent pool "abcdegh": 404 API error: agent pool not found`,
)
})
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestCreateAgentPool(t *testing.T) {
Name: name,
},
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 401,
Message: "unauthorized",
},
Expand Down Expand Up @@ -129,7 +129,7 @@ func TestGetAgentPool(t *testing.T) {
ExpectedReqPath: fmt.Sprintf("/api/orgs/%s/agent-pools/%s", org, id),
ExpectedReqBody: nil,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 401,
Message: "unauthorized",
},
Expand Down
6 changes: 3 additions & 3 deletions provider/pkg/internal/pulumiapi/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (c *Client) createRequest(ctx context.Context, method string, url *url.URL,
}

// sendRequest executes req and unmarshals response json into resBody
// returns attempts to unmarshal response into errorResponse if statusCode not 2XX
// returns attempts to unmarshal response into ErrorResponse if statusCode not 2XX
func (c *Client) sendRequest(req *http.Request, resBody interface{}) (*http.Response, error) {
res, err := c.httpClient.Do(req)
if err != nil {
Expand All @@ -91,9 +91,9 @@ func (c *Client) sendRequest(req *http.Request, resBody interface{}) (*http.Resp
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if !ok(res.StatusCode) {
// if we didn't get an 2XX status code, unmarshal the response as an errorResponse
// if we didn't get an 2XX status code, unmarshal the response as an ErrorResponse
// and return an error
var errRes errorResponse
var errRes ErrorResponse
err = json.Unmarshal(body, &errRes)
if err != nil {
return res, fmt.Errorf("failed to parse response body from url %q, status code %d: %w\n\n%s\n",
Expand Down
2 changes: 1 addition & 1 deletion provider/pkg/internal/pulumiapi/deployment_setting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestGetDeploymentSettings(t *testing.T) {
ExpectedReqMethod: http.MethodGet,
ExpectedReqPath: "/" + path.Join("api", "stacks", orgName, projectName, stackName, "deployments", "settings"),
ResponseCode: 404,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 404,
Message: "not found",
},
Expand Down
8 changes: 4 additions & 4 deletions provider/pkg/internal/pulumiapi/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ import (
"fmt"
)

// errorResponse is returned from pulumi service api when there's been an error
type errorResponse struct {
// ErrorResponse is returned from pulumi service api when there's been an error
type ErrorResponse struct {
StatusCode int `json:"code"`
Message string `json:"message"`
}

func (err *errorResponse) Error() string {
func (err *ErrorResponse) Error() string {
return fmt.Sprintf("%d API error: %s", err.StatusCode, err.Message)
}

func GetErrorStatusCode(err error) int {
var errResp *errorResponse
var errResp *ErrorResponse
if errors.As(err, &errResp) {
return errResp.StatusCode
}
Expand Down
6 changes: 3 additions & 3 deletions provider/pkg/internal/pulumiapi/members_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestAddMemberToOrg(t *testing.T) {
Role: role,
},
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -75,7 +75,7 @@ func TestListOrgMembers(t *testing.T) {
ExpectedReqMethod: http.MethodGet,
ExpectedReqPath: "/api/orgs/an-organization/members",
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -105,7 +105,7 @@ func TestDeleteMemberFromOrg(t *testing.T) {
ExpectedReqMethod: http.MethodDelete,
ExpectedReqPath: "/api/orgs/an-organization/members/a-user",
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down
6 changes: 3 additions & 3 deletions provider/pkg/internal/pulumiapi/orgtokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestDeleteOrgAccessToken(t *testing.T) {
ExpectedReqMethod: http.MethodDelete,
ExpectedReqPath: "/api/orgs/anOrg/tokens/" + tokenId,
ResponseCode: 404,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 404,
Message: "token not found",
},
Expand Down Expand Up @@ -105,7 +105,7 @@ func TestCreateOrgAccessToken(t *testing.T) {
Name: name,
},
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 401,
Message: "unauthorized",
},
Expand Down Expand Up @@ -162,7 +162,7 @@ func TestGetOrgAccessToken(t *testing.T) {
ExpectedReqPath: fmt.Sprintf("/api/orgs/%s/tokens", org),
ExpectedReqBody: nil,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 401,
Message: "unauthorized",
},
Expand Down
18 changes: 9 additions & 9 deletions provider/pkg/internal/pulumiapi/schedules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func TestCreateDeploymentSchedule(t *testing.T) {
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/schedules",
ExpectedReqBody: createDeploymentScheduleReq,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -95,7 +95,7 @@ func TestGetDeploymentSchedule(t *testing.T) {
ExpectedReqMethod: http.MethodGet,
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/schedules/" + testScheduleID,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand All @@ -110,7 +110,7 @@ func TestGetDeploymentSchedule(t *testing.T) {
ExpectedReqMethod: http.MethodGet,
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/schedules/" + testScheduleID,
ResponseCode: 404,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
StatusCode: 404,
Message: "not found",
},
Expand Down Expand Up @@ -144,7 +144,7 @@ func TestUpdateDeploymentSchedule(t *testing.T) {
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/schedules/" + testScheduleID,
ExpectedReqBody: createDeploymentScheduleReq,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -172,7 +172,7 @@ func TestDeleteSchedule(t *testing.T) {
ExpectedReqMethod: http.MethodDelete,
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/schedules/" + testScheduleID,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -204,7 +204,7 @@ func TestCreateDriftSchedule(t *testing.T) {
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/drift/schedules",
ExpectedReqBody: createDriftScheduleReq,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -237,7 +237,7 @@ func TestUpdateDriftSchedule(t *testing.T) {
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/drift/schedules/" + testScheduleID,
ExpectedReqBody: createDriftScheduleReq,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -270,7 +270,7 @@ func TestCreateTtlSchedule(t *testing.T) {
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/ttl/schedules",
ExpectedReqBody: createTtlScheduleReq,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -303,7 +303,7 @@ func TestUpdateTtlSchedule(t *testing.T) {
ExpectedReqPath: "/api/stacks/org/project/stack/deployments/ttl/schedules/" + testScheduleID,
ExpectedReqBody: createTtlScheduleReq,
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down
4 changes: 2 additions & 2 deletions provider/pkg/internal/pulumiapi/stack_tags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestCreateStackTags(t *testing.T) {
ExpectedReqMethod: http.MethodPost,
ExpectedReqPath: fmt.Sprintf("/api/stacks/%s/%s/%s/tags", stackName.OrgName, stackName.ProjectName, stackName.StackName),
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -68,7 +68,7 @@ func TestDeleteStackTags(t *testing.T) {
ExpectedReqMethod: http.MethodDelete,
ExpectedReqPath: "/api/stacks/organization/project/stack/tags/tagName",
ResponseCode: 401,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down
4 changes: 2 additions & 2 deletions provider/pkg/internal/pulumiapi/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestCreateStack(t *testing.T) {
ExpectedReqMethod: http.MethodPost,
ExpectedReqPath: fmt.Sprintf("/api/stacks/%s/%s", s.OrgName, s.ProjectName),
ResponseCode: http.StatusUnauthorized,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down Expand Up @@ -76,7 +76,7 @@ func TestDeleteStack(t *testing.T) {
ExpectedReqMethod: http.MethodDelete,
ExpectedReqPath: "/api/stacks/organization/project/stack",
ResponseCode: http.StatusUnauthorized,
ResponseBody: errorResponse{
ResponseBody: ErrorResponse{
Message: "unauthorized",
},
})
Expand Down
Loading

0 comments on commit 3445009

Please sign in to comment.