Skip to content

Commit

Permalink
SUP-863 Allow pipeline to be removed from a cluster (#279)
Browse files Browse the repository at this point in the history
* SUP-863 Add generated code for pipeline update

* SUP-863 Change ClusterId to pointer

* SUP-863 Add omitempty to some fields

* SUP-863 Fix removing pipeline from cluster

* SUP-863 Add graphqlrc.yml

* SUP-863 Update changelog with pending change

* SUP-863 Add test for removing from cluster
  • Loading branch information
jradtilbrook authored Jun 15, 2023
1 parent 1927e12 commit 291999a
Show file tree
Hide file tree
Showing 9 changed files with 1,018 additions and 80 deletions.
1 change: 1 addition & 0 deletions .graphqlrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
schema: schema.graphql
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.

## Unreleased

* Allow pipeline to be removed from a cluster [[PR #279](https://github.com/buildkite/terraform-provider-buildkite/pull/279)]
* Change default provider settings to match new pipeline [[PR #282](https://github.com/buildkite/terraform-provider-buildkite/pull/282)]

## [v0.18.1](https://github.com/buildkite/terraform-provider-buildkite/compare/v0.18.0...v0.18.1)
Expand Down
630 changes: 630 additions & 0 deletions buildkite/generated.go

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions buildkite/graphql/pipeline.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,62 @@ query getPipeline($slug: ID!) {
webhookURL
}
}

# @genqlient(for: "PipelineUpdateInput.clusterId", pointer: true)
# @genqlient(for: "PipelineUpdateInput.visibility", omitempty: true)
# @genqlient(for: "PipelineUpdateInput.buildRetentionPeriod", omitempty: true)
# @genqlient(for: "PipelineUpdateInput.buildRetentionNumber", omitempty: true)
# @genqlient(for: "PipelineUpdateInput.archived", omitempty: true)
# @genqlient(for: "PipelineUpdateInput.nextBuildNumber", omitempty: true)
mutation updatePipeline(
$input: PipelineUpdateInput!
) {
pipelineUpdate(input: $input) {
pipeline {
id
allowRebuilds
cancelIntermediateBuilds
cancelIntermediateBuildsBranchFilter
cluster {
id
}
defaultBranch
defaultTimeoutInMinutes
maximumTimeoutInMinutes
description
name
repository {
url
}
skipIntermediateBuilds
skipIntermediateBuildsBranchFilter
slug
steps {
yaml
}
tags {
label
}
teams (first: 50) {
edges {
node {
accessLevel
id
team {
description
id
isDefaultTeam
defaultMemberRole
name
membersCanCreatePipelines
privacy
slug
uuid
}
}
}
}
webhookURL
}
}
}
167 changes: 102 additions & 65 deletions buildkite/resource_pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,43 @@ const defaultSteps = `steps:
command: buildkite-agent pipeline upload`

// PipelineNode represents a pipeline as returned from the GraphQL API
type Cluster struct {
ID graphql.String
}
type Repository struct {
URL graphql.String
}
type Steps struct {
YAML graphql.String
}
type PipelineNode struct {
AllowRebuilds graphql.Boolean
CancelIntermediateBuilds graphql.Boolean
CancelIntermediateBuildsBranchFilter graphql.String
Cluster struct {
ID graphql.String
}
DefaultBranch graphql.String
DefaultTimeoutInMinutes graphql.Int
MaximumTimeoutInMinutes graphql.Int
Description graphql.String
ID graphql.String
Name graphql.String
Repository struct {
URL graphql.String
}
SkipIntermediateBuilds graphql.Boolean
SkipIntermediateBuildsBranchFilter graphql.String
Slug graphql.String
Steps struct {
YAML graphql.String
}
Tags []PipelineTag
Teams struct {
Cluster Cluster
DefaultBranch graphql.String
DefaultTimeoutInMinutes graphql.Int
MaximumTimeoutInMinutes graphql.Int
Description graphql.String
ID graphql.String
Name graphql.String
Repository Repository
SkipIntermediateBuilds graphql.Boolean
SkipIntermediateBuildsBranchFilter graphql.String
Slug graphql.String
Steps Steps
Tags []PipelineTag
Teams struct {
Edges []struct {
Node TeamPipelineNode
}
} `graphql:"teams(first: 50)"`
WebhookURL graphql.String `graphql:"webhookURL"`
}

// PipelineAccessLevels represents a pipeline access levels as returned from the GraphQL API
type PipelineAccessLevels graphql.String

type PipelineTag struct {
Label graphql.String
}
type PipelineTagInput struct {
Label graphql.String `json:"label"`
}

// TeamPipelineNode represents a team pipeline as returned from the GraphQL API
type TeamPipelineNode struct {
Expand Down Expand Up @@ -101,7 +98,6 @@ func resourcePipeline() *schema.Resource {
Type: schema.TypeString,
},
"cluster_id": {
Computed: true,
Optional: true,
Type: schema.TypeString,
},
Expand Down Expand Up @@ -435,58 +431,71 @@ func ReadPipeline(ctx context.Context, d *schema.ResourceData, m interface{}) di
func UpdatePipeline(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
client := m.(*Client)
var err error
var mutation struct {
PipelineUpdate struct {
Pipeline PipelineNode
} `graphql:"pipelineUpdate(input: {allowRebuilds: $allow_rebuilds, cancelIntermediateBuilds: $cancel_intermediate_builds, cancelIntermediateBuildsBranchFilter: $cancel_intermediate_builds_branch_filter, defaultBranch: $default_branch, defaultTimeoutInMinutes: $default_timeout_in_minutes, maximumTimeoutInMinutes: $maximum_timeout_in_minutes, description: $desc, id: $id, name: $name, repository: {url: $repository_url}, skipIntermediateBuilds: $skip_intermediate_builds, skipIntermediateBuildsBranchFilter: $skip_intermediate_builds_branch_filter, steps: {yaml: $steps}, tags: $tags})"`
}
vars := map[string]interface{}{
"allow_rebuilds": graphql.Boolean(d.Get("allow_rebuilds").(bool)),
"cancel_intermediate_builds": graphql.Boolean(d.Get("cancel_intermediate_builds").(bool)),
"cancel_intermediate_builds_branch_filter": graphql.String(d.Get("cancel_intermediate_builds_branch_filter").(string)),
"default_branch": graphql.String(d.Get("default_branch").(string)),
"default_timeout_in_minutes": graphql.Int(d.Get("default_timeout_in_minutes").(int)),
"maximum_timeout_in_minutes": graphql.Int(d.Get("maximum_timeout_in_minutes").(int)),
"desc": graphql.String(d.Get("description").(string)),
"id": graphql.ID(d.Id()),
"name": graphql.String(d.Get("name").(string)),
"repository_url": graphql.String(d.Get("repository").(string)),
"skip_intermediate_builds": graphql.Boolean(d.Get("skip_intermediate_builds").(bool)),
"skip_intermediate_builds_branch_filter": graphql.String(d.Get("skip_intermediate_builds_branch_filter").(string)),
"steps": graphql.String(d.Get("steps").(string)),
"tags": getTagsFromSchema(d),
}

log.Printf("Updating pipeline %s ...", vars["name"])

// If the cluster_id key is present in the mutation, GraphQL expects a valid ID.
// Check if cluster_id exists in the configuration before adding to mutation.
if clusterID, ok := d.GetOk("cluster_id"); ok {
var mutationWithClusterID struct {
PipelineUpdate struct {
Pipeline PipelineNode
} `graphql:"pipelineUpdate(input: {allowRebuilds: $allow_rebuilds, cancelIntermediateBuilds: $cancel_intermediate_builds, cancelIntermediateBuildsBranchFilter: $cancel_intermediate_builds_branch_filter, clusterId: $cluster_id, defaultBranch: $default_branch, defaultTimeoutInMinutes: $default_timeout_in_minutes, maximumTimeoutInMinutes: $maximum_timeout_in_minutes, description: $desc, id: $id, name: $name, repository: {url: $repository_url}, skipIntermediateBuilds: $skip_intermediate_builds, skipIntermediateBuildsBranchFilter: $skip_intermediate_builds_branch_filter, steps: {yaml: $steps}, tags: $tags})"`
input := PipelineUpdateInput{
AllowRebuilds: d.Get("allow_rebuilds").(bool),
CancelIntermediateBuilds: d.Get("cancel_intermediate_builds").(bool),
CancelIntermediateBuildsBranchFilter: d.Get("cancel_intermediate_builds_branch_filter").(string),
DefaultBranch: d.Get("default_branch").(string),
DefaultTimeoutInMinutes: d.Get("default_timeout_in_minutes").(int),
MaximumTimeoutInMinutes: d.Get("maximum_timeout_in_minutes").(int),
Description: d.Get("description").(string),
Id: d.Id(),
Name: d.Get("name").(string),
Repository: PipelineRepositoryInput{Url: d.Get("repository").(string)},
SkipIntermediateBuilds: d.Get("skip_intermediate_builds").(bool),
SkipIntermediateBuildsBranchFilter: d.Get("skip_intermediate_builds_branch_filter").(string),
Steps: PipelineStepsInput{Yaml: d.Get("steps").(string)},
Tags: getTagsFromSchema(d),
}

// If cluster_id exists in the schema it must be a non-empty string
// Otherwise, if its not present it will be set to nil by default
if clusterID, clusterIdPresent := d.GetOk("cluster_id"); clusterIdPresent {
if value, isString := clusterID.(string); isString && value != "" {
input.ClusterId = &value
}
vars["cluster_id"] = graphql.ID(clusterID.(string))
err = client.graphql.Mutate(context.Background(), &mutationWithClusterID, vars)
mutation.PipelineUpdate.Pipeline = mutationWithClusterID.PipelineUpdate.Pipeline
} else {
err = client.graphql.Mutate(context.Background(), &mutation, vars)
}

log.Printf("Updating pipeline %s ...", input.Name)

response, err := updatePipeline(client.genqlient, input)

if err != nil {
log.Printf("Unable to update pipeline %s", d.Get("name"))
return diag.FromErr(err)
}

// While transitioning from shurcool to genqlient, we'll map the response here to utilise existing functionality
pipeline := PipelineNode{
AllowRebuilds: graphql.Boolean(response.PipelineUpdate.Pipeline.AllowRebuilds),
CancelIntermediateBuilds: graphql.Boolean(response.PipelineUpdate.Pipeline.CancelIntermediateBuilds),
CancelIntermediateBuildsBranchFilter: graphql.String(response.PipelineUpdate.Pipeline.CancelIntermediateBuildsBranchFilter),
Cluster: Cluster{ID: graphql.String(response.PipelineUpdate.Pipeline.Cluster.Id)},
DefaultBranch: graphql.String(response.PipelineUpdate.Pipeline.DefaultBranch),
DefaultTimeoutInMinutes: graphql.Int(response.PipelineUpdate.Pipeline.DefaultTimeoutInMinutes),
MaximumTimeoutInMinutes: graphql.Int(response.PipelineUpdate.Pipeline.MaximumTimeoutInMinutes),
Description: graphql.String(response.PipelineUpdate.Pipeline.Description),
ID: graphql.String(response.PipelineUpdate.Pipeline.Id),
Name: graphql.String(response.PipelineUpdate.Pipeline.Name),
Repository: Repository{URL: graphql.String(response.PipelineUpdate.Pipeline.Repository.Url)},
SkipIntermediateBuilds: graphql.Boolean(response.PipelineUpdate.Pipeline.SkipIntermediateBuilds),
SkipIntermediateBuildsBranchFilter: graphql.String(response.PipelineUpdate.Pipeline.SkipIntermediateBuildsBranchFilter),
Slug: graphql.String(response.PipelineUpdate.Pipeline.Slug),
Steps: Steps{YAML: graphql.String(response.PipelineUpdate.Pipeline.Steps.Yaml)},
Tags: mapTagsFromGenqlient(response.PipelineUpdate.Pipeline.Tags),
Teams: mapTeamPipelinesFromGenqlient(response.PipelineUpdate.Pipeline.Teams.Edges),
WebhookURL: graphql.String(response.PipelineUpdate.Pipeline.WebhookURL),
}

teamPipelines := getTeamPipelinesFromSchema(d)
err = reconcileTeamPipelines(teamPipelines, &mutation.PipelineUpdate.Pipeline, client)
err = reconcileTeamPipelines(teamPipelines, &pipeline, client)
if err != nil {
log.Print("Unable to reconcile team pipelines")
return diag.FromErr(err)
}

updatePipelineResource(d, &mutation.PipelineUpdate.Pipeline)
updatePipelineResource(d, &pipeline)

pipelineExtraInfo, err := updatePipelineExtraInfo(d, client)
if err != nil {
Expand Down Expand Up @@ -589,17 +598,45 @@ func updatePipelineExtraInfo(d *schema.ResourceData, client *Client) (PipelineEx
return pipelineExtraInfo, nil
}

func mapTagsFromGenqlient(tags []updatePipelinePipelineUpdatePipelineUpdatePayloadPipelineTagsPipelineTag) []PipelineTag {
newTags := make([]PipelineTag, len(tags))

for i, v := range tags {
newTags[i] = PipelineTag{Label: graphql.String(v.Label)}
}
return newTags
}

func getTagsFromSchema(d *schema.ResourceData) []PipelineTagInput {
tagSet := d.Get("tags").(*schema.Set)
tags := make([]PipelineTagInput, tagSet.Len())
for i, v := range tagSet.List() {
tags[i] = PipelineTagInput{
Label: graphql.String(v.(string)),
Label: v.(string),
}
}
return tags
}

func mapTeamPipelinesFromGenqlient(tags []updatePipelinePipelineUpdatePipelineUpdatePayloadPipelineTeamsTeamPipelineConnectionEdgesTeamPipelineEdge) struct {
Edges []struct{ Node TeamPipelineNode }
} {
teamPipelineNodes := make([]struct{ Node TeamPipelineNode }, len(tags))
for i, v := range tags {
teamPipelineNodes[i] = struct{ Node TeamPipelineNode }{
Node: TeamPipelineNode{
AccessLevel: v.Node.AccessLevel,
ID: graphql.String(v.Node.Id),
Team: TeamNode{
Slug: graphql.String(v.Node.Team.Slug),
}},
}
}
return struct {
Edges []struct{ Node TeamPipelineNode }
}{Edges: teamPipelineNodes}
}

func getTeamPipelinesFromSchema(d *schema.ResourceData) []TeamPipelineNode {
teamsInput := d.Get("team").(*schema.Set).List()
teamPipelineNodes := make([]TeamPipelineNode, len(teamsInput))
Expand Down
46 changes: 42 additions & 4 deletions buildkite/resource_pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestAccPipeline_add_remove(t *testing.T) {
}

// Confirm that we can create a new pipeline with a cluster, and then delete it without error
func TestAccPipeline_add_remove_withcluster(t *testing.T) {
func TestAccPipeline_add_delete_withcluster(t *testing.T) {
var resourcePipeline PipelineNode

resource.Test(t, resource.TestCase{
Expand Down Expand Up @@ -73,6 +73,45 @@ func TestAccPipeline_add_remove_withcluster(t *testing.T) {
})
}

// Confirm that we can create a new pipeline with a cluster, and then remove it from the cluster
func TestAccPipeline_add_remove_withcluster(t *testing.T) {
var resourcePipeline PipelineNode

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckPipelineResourceDestroy,
Steps: []resource.TestStep{
{
Config: testAccPipelineConfigBasicWithCluster("foo"),
Check: resource.ComposeAggregateTestCheckFunc(
// Confirm the pipeline exists in the buildkite API
testAccCheckPipelineExists("buildkite_pipeline.foobar", &resourcePipeline),
// Confirm the pipeline has the correct values in Buildkite's system
testAccCheckPipelineRemoteValues(&resourcePipeline, "Test Pipeline foo"),
// Confirm the pipeline has the correct values in terraform state
resource.TestCheckResourceAttr("buildkite_pipeline.foobar", "name", "Test Pipeline foo"),
resource.TestCheckResourceAttr("buildkite_pipeline.foobar", "cluster_id", "Q2x1c3Rlci0tLTRlN2JmM2FjLWUzMjMtNGY1OS05MGY2LTQ5OTljZmI2MGQyYg=="),
resource.TestCheckResourceAttr("buildkite_pipeline.foobar", "allow_rebuilds", "true"),
),
},
{
Config: testAccPipelineConfigBasicWithTeam("foo"),
Check: resource.ComposeAggregateTestCheckFunc(
// Confirm the pipeline exists in the buildkite API
testAccCheckPipelineExists("buildkite_pipeline.foobar", &resourcePipeline),
// Confirm the pipeline has the correct values in Buildkite's system
testAccCheckPipelineRemoteValues(&resourcePipeline, "Test Pipeline foo"),
// Confirm the pipeline has the correct values in terraform state
resource.TestCheckResourceAttr("buildkite_pipeline.foobar", "name", "Test Pipeline foo"),
resource.TestCheckResourceAttr("buildkite_pipeline.foobar", "cluster_id", ""),
resource.TestCheckResourceAttr("buildkite_pipeline.foobar", "allow_rebuilds", "true"),
),
},
},
})
}

func TestAccPipeline_add_remove_complex(t *testing.T) {
var resourcePipeline PipelineNode
steps := `"steps:\n- command: buildkite-agent pipeline upload\n"`
Expand Down Expand Up @@ -540,9 +579,8 @@ func testAccPipelineConfigBasicWithCluster(name string) string {
resource "buildkite_pipeline" "foobar" {
name = "Test Pipeline %s"
repository = "https://github.com/buildkite/terraform-provider-buildkite.git"
steps = ""
cluster_id = "Q2x1c3Rlci0tLTRlN2JmM2FjLWUzMjMtNGY1OS05MGY2LTQ5OTljZmI2MGQyYg=="
allow_rebuilds = true
cluster_id = "Q2x1c3Rlci0tLTRlN2JmM2FjLWUzMjMtNGY1OS05MGY2LTQ5OTljZmI2MGQyYg=="
allow_rebuilds = true
team {
slug = "everyone"
Expand Down
3 changes: 0 additions & 3 deletions buildkite/resource_team.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import (
"github.com/shurcooL/graphql"
)

type TeamPrivacy graphql.String
type TeamMemberRole graphql.String

type TeamNode struct {
Description graphql.String
ID graphql.String
Expand Down
4 changes: 4 additions & 0 deletions genqlient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ generated: buildkite/generated.go

# We pass context.Background() everywhere so just leave it off
context_type: "-"

bindings:
YAML:
type: string
Loading

0 comments on commit 291999a

Please sign in to comment.