Skip to content

Commit 41f6fa7

Browse files
authored
feat: add buildkite_registry data resource (#937)
newRegistryDatasource simplify
1 parent 4fa05d8 commit 41f6fa7

File tree

5 files changed

+343
-0
lines changed

5 files changed

+343
-0
lines changed

buildkite/data_source_registry.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package buildkite
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strings"
10+
11+
"github.com/MakeNowJust/heredoc"
12+
"github.com/hashicorp/terraform-plugin-framework/attr"
13+
"github.com/hashicorp/terraform-plugin-framework/datasource"
14+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
15+
"github.com/hashicorp/terraform-plugin-framework/types"
16+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
17+
)
18+
19+
// Ensure provider defined types fully satisfy framework interfaces
20+
var _ datasource.DataSource = &registryDatasource{}
21+
var _ datasource.DataSourceWithConfigure = &registryDatasource{}
22+
23+
func newRegistryDatasource() datasource.DataSource {
24+
return &registryDatasource{}
25+
}
26+
27+
type registryDatasource struct {
28+
client *Client
29+
}
30+
31+
type registryDatasourceModel struct {
32+
ID types.String `tfsdk:"id"` // GraphQL ID
33+
UUID types.String `tfsdk:"uuid"` // UUID
34+
Name types.String `tfsdk:"name"` // Name
35+
Slug types.String `tfsdk:"slug"` // Slug (used for lookup)
36+
Ecosystem types.String `tfsdk:"ecosystem"` // PackageEcosystem
37+
Description types.String `tfsdk:"description"` // Description
38+
Emoji types.String `tfsdk:"emoji"` // Emoji
39+
Color types.String `tfsdk:"color"` // Color
40+
OIDCPolicy types.String `tfsdk:"oidc_policy"` // OIDC Policy
41+
TeamIDs types.List `tfsdk:"team_ids"` // List of Team GraphQL IDs
42+
}
43+
44+
func (d *registryDatasource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
45+
resp.TypeName = req.ProviderTypeName + "_registry"
46+
}
47+
48+
func (d *registryDatasource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
49+
if req.ProviderData == nil {
50+
return
51+
}
52+
d.client = req.ProviderData.(*Client)
53+
}
54+
55+
func (d *registryDatasource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
56+
resp.Schema = schema.Schema{
57+
MarkdownDescription: heredoc.Doc(`
58+
Use this data source to retrieve information about a Buildkite Package Registry.
59+
60+
A package registry is a private repository for your organization's packages.
61+
See https://buildkite.com/docs/packages for more information.
62+
`),
63+
Attributes: map[string]schema.Attribute{
64+
"id": schema.StringAttribute{
65+
Computed: true,
66+
MarkdownDescription: "The GraphQL ID of the registry.",
67+
},
68+
"uuid": schema.StringAttribute{
69+
Computed: true,
70+
MarkdownDescription: "The UUID of the registry.",
71+
},
72+
"name": schema.StringAttribute{
73+
Computed: true,
74+
MarkdownDescription: "The name of the registry.",
75+
},
76+
"slug": schema.StringAttribute{
77+
Required: true,
78+
MarkdownDescription: "The slug of the registry. This is used to identify the registry.",
79+
},
80+
"ecosystem": schema.StringAttribute{
81+
Computed: true,
82+
MarkdownDescription: "The ecosystem of the registry (e.g. `NPM`, `RUBYGEMS`, `DOCKER`).",
83+
},
84+
"description": schema.StringAttribute{
85+
Computed: true,
86+
MarkdownDescription: "A description for the registry.",
87+
},
88+
"emoji": schema.StringAttribute{
89+
Computed: true,
90+
MarkdownDescription: "An emoji to use with the registry.",
91+
},
92+
"color": schema.StringAttribute{
93+
Computed: true,
94+
MarkdownDescription: "A color representation of the registry.",
95+
},
96+
"oidc_policy": schema.StringAttribute{
97+
Computed: true,
98+
MarkdownDescription: "The registry's OIDC policy.",
99+
},
100+
"team_ids": schema.ListAttribute{
101+
Computed: true,
102+
ElementType: types.StringType,
103+
MarkdownDescription: "A list of team GraphQL IDs that have access to this registry.",
104+
},
105+
},
106+
}
107+
}
108+
109+
func (d *registryDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
110+
var state registryDatasourceModel
111+
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...) // Use req.Config for data sources
112+
if resp.Diagnostics.HasError() {
113+
return
114+
}
115+
116+
timeoutDuration, diags := d.client.timeouts.Read(ctx, DefaultTimeout)
117+
resp.Diagnostics.Append(diags...)
118+
if resp.Diagnostics.HasError() {
119+
return
120+
}
121+
122+
var dataFound bool
123+
124+
err := retry.RetryContext(ctx, timeoutDuration, func() *retry.RetryError {
125+
rawSlug := state.Slug.ValueString()
126+
var fullSlug string
127+
if strings.Contains(rawSlug, "/") {
128+
fullSlug = rawSlug
129+
} else {
130+
fullSlug = fmt.Sprintf("%s/%s", d.client.organization, rawSlug)
131+
}
132+
133+
apiPathSlug := rawSlug
134+
if i := strings.LastIndexByte(rawSlug, '/'); i != -1 {
135+
apiPathSlug = rawSlug[i+1:]
136+
}
137+
138+
url := fmt.Sprintf("%s/v2/packages/organizations/%s/registries/%s", d.client.restURL, d.client.organization, apiPathSlug)
139+
140+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
141+
if err != nil {
142+
return retry.NonRetryableError(fmt.Errorf("error creating HTTP request: %w", err))
143+
}
144+
req.Header.Set("Accept", "application/json")
145+
146+
httpResp, err := d.client.http.Do(req)
147+
if err != nil {
148+
return retry.RetryableError(fmt.Errorf("error making HTTP request to %s: %w", url, err))
149+
}
150+
defer httpResp.Body.Close()
151+
152+
if httpResp.StatusCode == http.StatusNotFound {
153+
resp.Diagnostics.AddWarning("Registry not found", fmt.Sprintf("No registry found with slug '%s' (resolved to '%s') at %s", state.Slug.ValueString(), fullSlug, url))
154+
resp.State.RemoveResource(ctx)
155+
dataFound = false
156+
return nil
157+
}
158+
159+
if httpResp.StatusCode >= 400 {
160+
bodyBytes, _ := io.ReadAll(httpResp.Body)
161+
return retry.RetryableError(fmt.Errorf("error fetching registry (status %d from %s): %s", httpResp.StatusCode, url, string(bodyBytes)))
162+
}
163+
164+
var result struct {
165+
GraphqlID string `json:"graphql_id"`
166+
ID string `json:"id"` // This is the UUID
167+
Slug string `json:"slug"`
168+
Name string `json:"name"`
169+
Ecosystem string `json:"ecosystem"`
170+
Description string `json:"description"`
171+
Emoji string `json:"emoji"`
172+
Color string `json:"color"`
173+
OIDCPolicy string `json:"oidc_policy"`
174+
TeamIDs []string `json:"team_ids"`
175+
}
176+
177+
if err := json.NewDecoder(httpResp.Body).Decode(&result); err != nil {
178+
return retry.NonRetryableError(fmt.Errorf("error decoding registry response body from %s: %w", url, err))
179+
}
180+
181+
if result.GraphqlID == "" { // Check if essential data is missing
182+
resp.Diagnostics.AddWarning("Registry data incomplete", fmt.Sprintf("Registry found with slug '%s' but essential data (GraphqlID) is missing from response at %s", fullSlug, url))
183+
resp.State.RemoveResource(ctx)
184+
dataFound = false
185+
return nil
186+
}
187+
188+
dataFound = true
189+
state.ID = types.StringValue(result.GraphqlID)
190+
state.UUID = types.StringValue(result.ID)
191+
state.Name = types.StringValue(result.Name)
192+
state.Slug = types.StringValue(result.Slug) // Re-affirm from response (this should be the simple slug)
193+
state.Ecosystem = types.StringValue(result.Ecosystem)
194+
195+
if result.Description != "" || !state.Description.IsNull() { // Update if API provides it or clear if API clears it and it was set
196+
state.Description = types.StringValue(result.Description)
197+
}
198+
if result.Emoji != "" || !state.Emoji.IsNull() {
199+
state.Emoji = types.StringValue(result.Emoji)
200+
}
201+
if result.Color != "" || !state.Color.IsNull() {
202+
state.Color = types.StringValue(result.Color)
203+
}
204+
if result.OIDCPolicy != "" || !state.OIDCPolicy.IsNull() {
205+
state.OIDCPolicy = types.StringValue(result.OIDCPolicy)
206+
}
207+
208+
// Handle team_ids using logic similar to handleTeamIDs from resource_registry.go
209+
if len(result.TeamIDs) > 0 {
210+
teams := make([]attr.Value, len(result.TeamIDs))
211+
for i, id := range result.TeamIDs {
212+
teams[i] = types.StringValue(id)
213+
}
214+
state.TeamIDs = types.ListValueMust(types.StringType, teams)
215+
} else {
216+
state.TeamIDs = types.ListNull(types.StringType) // If API returns empty, set to null
217+
}
218+
219+
return nil
220+
})
221+
222+
if err != nil {
223+
resp.Diagnostics.AddError("Failed to query Registry", fmt.Sprintf("Failed to query Registry with slug '%s' after multiple attempts: %s", state.Slug.ValueString(), err.Error()))
224+
return
225+
}
226+
227+
if dataFound {
228+
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
229+
}
230+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package buildkite
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
8+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
9+
)
10+
11+
func TestAccDataSourceRegistry_Basic(t *testing.T) {
12+
randName := acctest.RandString(10)
13+
var resourceName = "buildkite_registry.test_reg"
14+
var dataSourceName = "data.buildkite_registry.data_test_reg"
15+
16+
resource.ParallelTest(t, resource.TestCase{
17+
PreCheck: func() { testAccPreCheck(t) },
18+
ProtoV6ProviderFactories: protoV6ProviderFactories(),
19+
Steps: []resource.TestStep{
20+
{
21+
Config: testAccDataSourceRegistryConfigBasic(randName),
22+
Check: resource.ComposeAggregateTestCheckFunc(
23+
// Check resource attributes
24+
resource.TestCheckResourceAttr(resourceName, "name", randName),
25+
resource.TestCheckResourceAttr(resourceName, "ecosystem", "java"),
26+
resource.TestCheckResourceAttr(resourceName, "emoji", ":package:"),
27+
resource.TestCheckResourceAttrSet(resourceName, "id"),
28+
resource.TestCheckResourceAttrSet(resourceName, "uuid"),
29+
resource.TestCheckResourceAttrSet(resourceName, "slug"),
30+
resource.TestCheckResourceAttr(resourceName, "team_ids.0", "31529c8a-7cfa-42e8-bb85-4c844a983ea0"),
31+
32+
// Check data source attributes against resource attributes
33+
resource.TestCheckResourceAttrPair(dataSourceName, "id", resourceName, "id"),
34+
resource.TestCheckResourceAttrPair(dataSourceName, "uuid", resourceName, "uuid"),
35+
resource.TestCheckResourceAttrPair(dataSourceName, "name", resourceName, "name"),
36+
resource.TestCheckResourceAttrPair(dataSourceName, "slug", resourceName, "slug"),
37+
resource.TestCheckResourceAttrPair(dataSourceName, "description", resourceName, "description"),
38+
resource.TestCheckResourceAttrPair(dataSourceName, "emoji", resourceName, "emoji"),
39+
resource.TestCheckResourceAttrPair(dataSourceName, "color", resourceName, "color"),
40+
// The REST API for a single registry doesn't return a full 'url' field directly.
41+
// resource.TestCheckResourceAttrPair(dataSourceName, "url", resourceName, "url"),
42+
resource.TestCheckResourceAttrPair(dataSourceName, "team_ids", resourceName, "team_ids"),
43+
44+
// Check attributes now available via REST API
45+
resource.TestCheckResourceAttrPair(dataSourceName, "ecosystem", resourceName, "ecosystem"),
46+
resource.TestCheckResourceAttrPair(dataSourceName, "oidc_policy", resourceName, "oidc_policy"),
47+
),
48+
},
49+
},
50+
})
51+
}
52+
53+
func testAccDataSourceRegistryConfigBasic(name string) string {
54+
// Using a known team ID from the existing resource_registry_test.go.
55+
const knownTeamID = "31529c8a-7cfa-42e8-bb85-4c844a983ea0"
56+
57+
return fmt.Sprintf(`
58+
provider "buildkite" {}
59+
60+
resource "buildkite_registry" "test_reg" {
61+
name = "%s"
62+
ecosystem = "java"
63+
description = "A test registry for data source testing."
64+
emoji = ":package:"
65+
color = "#123456"
66+
team_ids = ["%s"]
67+
}
68+
69+
data "buildkite_registry" "data_test_reg" {
70+
slug = buildkite_registry.test_reg.slug
71+
}
72+
`, name, knownTeamID)
73+
}

buildkite/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func (*terraformProvider) DataSources(context.Context) []func() datasource.DataS
127127
newOrganizationRuleDatasource,
128128
newPipelineDatasource,
129129
newPipelineTemplateDatasource,
130+
newRegistryDatasource,
130131
newSignedPipelineStepsDataSource,
131132
newTeamDatasource,
132133
newTestSuiteDatasource,

docs/data-sources/registry.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "buildkite_registry Data Source - terraform-provider-buildkite"
4+
subcategory: ""
5+
description: |-
6+
Use this data source to retrieve information about a Buildkite Package Registry.
7+
A package registry is a private repository for your organization's packages.
8+
See https://buildkite.com/docs/packages for more information.
9+
---
10+
11+
# buildkite_registry (Data Source)
12+
13+
Use this data source to retrieve information about a Buildkite Package Registry.
14+
15+
A package registry is a private repository for your organization's packages.
16+
See https://buildkite.com/docs/packages for more information.
17+
18+
19+
20+
<!-- schema generated by tfplugindocs -->
21+
## Schema
22+
23+
### Required
24+
25+
- `slug` (String) The slug of the registry. This is used to identify the registry.
26+
27+
### Read-Only
28+
29+
- `color` (String) A color representation of the registry.
30+
- `description` (String) A description for the registry.
31+
- `ecosystem` (String) The ecosystem of the registry (e.g. `NPM`, `RUBYGEMS`, `DOCKER`).
32+
- `emoji` (String) An emoji to use with the registry.
33+
- `id` (String) The GraphQL ID of the registry.
34+
- `name` (String) The name of the registry.
35+
- `oidc_policy` (String) The registry's OIDC policy.
36+
- `team_ids` (List of String) A list of team GraphQL IDs that have access to this registry.
37+
- `uuid` (String) The UUID of the registry.

mise.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tools]
2+
go = "1.24.3"

0 commit comments

Comments
 (0)