Skip to content

Commit

Permalink
SUP-2506: Organization rule resource and data source implementation (#…
Browse files Browse the repository at this point in the history
…562)

* First organization rule resource/test implementation, GQL schema

* OrganizationRule GraphQL, generated helper code

* Update, Delete rules, generated code

* Initial Read through node query - will need schema updated

* Successful plan of Creates (matching upper in plan/resource attributes)

* Required attributes only name/value - tightened Create logic

* UUIDs to schema, fragment fields

* Initial rule create test

* Rule Datasource first implementation

* Fine tuning source/target UUID fetching on rule creation

* Tidied up source/target and value JSON read funcs

* Added ARTIFACTS_READ value JSON reads, fixed TargetUUID state save for creates

* Docs generation, slight MarkdownDescription alteration for resource

* Datasource docs, example, touched up resource docs

* Added additional organization rule resource tests (TRIGGER_BUILD, ARTIFACTS_READ creates/imports)

* Added organization rule datasource tests (TRIGGER_BUILD, ARTIFACTS_READ)

* Schema update, name->type conversion for rule resource/DSes, tests, docs

* Rules resource docs example refresh for name->type refactor

* A small capitalization amendment, rule creation state model func

* updateOrganizatonRuleReadState reword for Rule resource tests

* Docs alignment for the source|target_pipeline_uuid keys in a rule document

* Double up characters, unknown action test in organizaton rule resource test

* More corner-case, erroneous rule resource tests

* Some wording and plan modifier corrections

* Linting, switched to organizationRuleDatasourceModel

* Linting around diagnostic error checks on Rule creation

* Added dependencies in rule resource tests

* Templated/rendered docs for organization rules, opt-in note

* Some rewording for opting in/rules in development

* And even better rewording

* Spelling greatness, default erroring for obtaining source/target UUIDs on create

* Changelog, some more obtainCreationUUIDs commentary

* [skip ci] Changelog number

* Added optional description attribute, rule resource tests extensibility, data source using correct schema

* Extended rule data source tests with required/all attribute loading, some corrections

* Fixed dupe `pipeline.trigger_build.pipeline` all attributes rule resource tests

* Added uuid data source reads, ranged data source tests and UUIID read addition, docs refresh

* I left in an empty space

* Refactored range loop for data source tests to refer to a rule `action`

* Ranged rule resource tests (creates, imports)

* source|target_pipeline_uuid -> source|target_pipeline

* UUID validation on rule creates, slight test renaming

* Quick lints

* Added rule document via GQL, conditions (un)+marshalling to rule resource/data source value in state

* Conditions to rule create tests, data source load all tests

* Storing document returned value from rule creates, conditions invalid error test

* Added required attribute rule resource import

* Realigned docs link

* IDed + extra logging
  • Loading branch information
james2791 authored Oct 15, 2024
1 parent 6a4c06c commit 3fc0a27
Show file tree
Hide file tree
Showing 21 changed files with 4,440 additions and 697 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- SUP-2506: Organization rule resource and data source implementation [[PR #562](https://github.com/buildkite/terraform-provider-buildkite/pull/562)] @james2791

## [v1.11.0](https://github.com/buildkite/terraform-provider-buildkite/compare/v1.10.2...v1.11.0)

- SUP-2536: Asserting pipeline template datasource attributes on its tests [[PR #559](https://github.com/buildkite/terraform-provider-buildkite/pull/559)] @james2791
Expand Down
271 changes: 271 additions & 0 deletions buildkite/data_source_organization_rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package buildkite

import (
"context"
"fmt"
"log"

"github.com/MakeNowJust/heredoc"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
)

type organizationRuleDatasourceModel struct {
ID types.String `tfsdk:"id"`
UUID types.String `tfsdk:"uuid"`
Description types.String `tfsdk:"description"`
Type types.String `tfsdk:"type"`
Value types.String `tfsdk:"value"`
SourceType types.String `tfsdk:"source_type"`
SourceUUID types.String `tfsdk:"source_uuid"`
TargetType types.String `tfsdk:"target_type"`
TargetUUID types.String `tfsdk:"target_uuid"`
Effect types.String `tfsdk:"effect"`
Action types.String `tfsdk:"action"`
}

type organizationRuleDatasource struct {
client *Client
}

func newOrganizationRuleDatasource() datasource.DataSource {
return &organizationRuleDatasource{}
}

func (organizationRuleDatasource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_organization_rule"
}

func (or *organizationRuleDatasource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

or.client = req.ProviderData.(*Client)
}

func (organizationRuleDatasource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: heredoc.Doc(`
Use this data source to retrieve an organization rule by its ID.
More information on organization rules can be found in the [documentation](https://buildkite.com/docs/pipelines/rules).
`),
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The GraphQL ID of the organization rule. ",
},
"uuid": schema.StringAttribute{
Optional: true,
MarkdownDescription: "The UUID of the organization rule. ",
},
"description": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The description of the organization rule. ",
},
"type": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The type of organization rule. ",
},
"value": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The JSON document that this organization rule implements. ",
},
"source_type": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The source resource type that this organization rule allows or denies to invoke its defined action. ",
},
"source_uuid": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The UUID of the resource that this organization rule allows or denies invocating its defined action. ",
},
"target_type": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The target resource type that this organization rule allows or denies the source to respective action. ",
},
"target_uuid": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The UUID of the target resourcee that this organization rule allows or denies invocation its respective action. ",
},
"effect": schema.StringAttribute{
Computed: true,
MarkdownDescription: "Whether this organization rule allows or denys the action to take place between source and target resources. ",
},
"action": schema.StringAttribute{
Computed: true,
MarkdownDescription: "The action defined between source and target resources. ",
},
},
}
}

func (or *organizationRuleDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var state organizationRuleDatasourceModel

resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

timeouts, diags := or.client.timeouts.Read(ctx, DefaultTimeout)
resp.Diagnostics.Append(diags...)

if resp.Diagnostics.HasError() {
return
}

// If a UUID is entered through an organization rule data source
if !state.UUID.IsNull() {
matchFound := false
err := retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
var cursor *string
for {
r, err := getOrganizationRules(
ctx,
or.client.genqlient,
or.client.organization,
cursor)
if err != nil {
if isRetryableError(err) {
return retry.RetryableError(err)
}
resp.Diagnostics.AddError(
"Unable to read organizatiion rules",
fmt.Sprintf("Unable to read organizatiion rules: %s", err.Error()),
)
return retry.NonRetryableError(err)
}

for _, rule := range r.Organization.Rules.Edges {
if rule.Node.Uuid == state.UUID.ValueString() {
matchFound = true
// Update data source state from the found rule
value, err := obtainValueJSON(rule.Node.Document)
if err != nil {
resp.Diagnostics.AddError(
"Unable to read organization rule",
fmt.Sprintf("Unable to read organmization rule: %s", err.Error()),
)
}
updateOrganizatonRuleDatasourceFromNode(&state, rule.Node, *value)
break
}
}

// If there is a match, or there is no next page, break
if matchFound || !r.Organization.Rules.PageInfo.HasNextPage {
break
}

// Move to the next cursor
cursor = &r.Organization.Rules.PageInfo.EndCursor
}
return nil
})

if err != nil {
resp.Diagnostics.AddError("Unable to find organization rule", err.Error())
return
}

if !matchFound {
resp.Diagnostics.AddError("Unable to find organization rule",
fmt.Sprintf("Could not find an organization rule with UUID \"%s\"", state.UUID.ValueString()))
return
}
// Otherwise if a ID is specified
} else if !state.ID.IsNull() {
var apiResponse *getNodeResponse
err := retry.RetryContext(ctx, timeouts, func() *retry.RetryError {
var err error

log.Printf("Reading organization rule with ID %s ...", state.ID.ValueString())
apiResponse, err = getNode(ctx,
or.client.genqlient,
state.ID.ValueString(),
)

return retryContextError(err)
})

if err != nil {
resp.Diagnostics.AddError(
"Unable to read organization rule",
fmt.Sprintf("Unable to read organmization rule: %s", err.Error()),
)
return
}

// Convert fron Node to getNodeNodeRule type
if organizationRule, ok := apiResponse.GetNode().(*getNodeNodeRule); ok {
if organizationRule == nil {
resp.Diagnostics.AddError(
"Unable to get organization rule",
"Error getting organization rule: nil response",
)
return
}
// Update data source state from the found rule
value, err := obtainValueJSON(organizationRule.Document)
if err != nil {
resp.Diagnostics.AddError(
"Unable to read organization rule",
fmt.Sprintf("Unable to read organmization rule: %s", err.Error()),
)
}
updateOrganizatonRuleDatasourceState(&state, *organizationRule, *value)
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

func obtainDatasourceReadUUIDs(orn getOrganizationRulesOrganizationRulesRuleConnectionEdgesRuleEdgeNodeRule) (string, string) {
var sourceUUID, targetUUID string

switch orn.SourceType {
case "PIPELINE":
sourceUUID = orn.Source.(*OrganizationRuleFieldsSourcePipeline).Uuid
}

switch orn.TargetType {
case "PIPELINE":
targetUUID = orn.Target.(*OrganizationRuleFieldsTargetPipeline).Uuid
}

return sourceUUID, targetUUID
}

func updateOrganizatonRuleDatasourceState(or *organizationRuleDatasourceModel, orn getNodeNodeRule, value string) {
sourceUUID, targetUUID := obtainReadUUIDs(orn)

or.ID = types.StringValue(orn.Id)
or.UUID = types.StringValue(orn.Uuid)
or.Description = types.StringPointerValue(orn.Description)
or.Type = types.StringValue(orn.Type)
or.Value = types.StringValue(value)
or.SourceType = types.StringValue(string(orn.SourceType))
or.SourceUUID = types.StringValue(sourceUUID)
or.TargetType = types.StringValue(string(orn.TargetType))
or.TargetUUID = types.StringValue(targetUUID)
or.Effect = types.StringValue(string(orn.Effect))
or.Action = types.StringValue(string(orn.Action))
}

func updateOrganizatonRuleDatasourceFromNode(or *organizationRuleDatasourceModel, orn getOrganizationRulesOrganizationRulesRuleConnectionEdgesRuleEdgeNodeRule, value string) {
sourceUUID, targetUUID := obtainDatasourceReadUUIDs(orn)

or.ID = types.StringValue(orn.Id)
or.UUID = types.StringValue(orn.Uuid)
or.Description = types.StringPointerValue(orn.Description)
or.Type = types.StringValue(orn.Type)
or.Value = types.StringValue(value)
or.SourceType = types.StringValue(string(orn.SourceType))
or.SourceUUID = types.StringValue(sourceUUID)
or.TargetType = types.StringValue(string(orn.TargetType))
or.TargetUUID = types.StringValue(targetUUID)
or.Effect = types.StringValue(string(orn.Effect))
or.Action = types.StringValue(string(orn.Action))
}
Loading

0 comments on commit 3fc0a27

Please sign in to comment.