Skip to content

Commit 0706eb6

Browse files
authored
TPT-4297: terraform: Implement linode_tag Data Source with Reserved IPv4 Support (#2330)
1 parent 84fb75d commit 0706eb6

9 files changed

Lines changed: 444 additions & 0 deletions

docs/data-sources/tag.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
page_title: "Linode: linode_tag"
3+
description: |-
4+
Provides details about a Linode Tag.
5+
---
6+
7+
# Data Source: linode\_tag
8+
9+
Provides information about a Linode Tag, including the objects associated with it.
10+
11+
For more information, see the [Linode APIv4 documentation](https://techdocs.akamai.com/linode-api/reference/get-tagged-objects).
12+
13+
## Example Usage
14+
15+
```hcl
16+
data "linode_tag" "example" {
17+
label = "my-tag"
18+
}
19+
```
20+
21+
## Argument Reference
22+
23+
* `label` - (Required) The label of the tag to look up.
24+
25+
## Attributes Reference
26+
27+
* `id` - The label of the tag.
28+
29+
* `objects` - A list of objects associated with this tag. Each object has the following attributes:
30+
31+
* `type` - The type of the tagged object (e.g. `linode`, `domain`, `volume`, `nodebalancer`, `reserved_ipv4_address`).
32+
33+
* `id` - The ID of the tagged object. For `reserved_ipv4_address` objects, this is the IP address string.

linode/framework_provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ import (
101101
"github.com/linode/terraform-provider-linode/v3/linode/sshkeys"
102102
"github.com/linode/terraform-provider-linode/v3/linode/stackscript"
103103
"github.com/linode/terraform-provider-linode/v3/linode/stackscripts"
104+
"github.com/linode/terraform-provider-linode/v3/linode/tag"
104105
"github.com/linode/terraform-provider-linode/v3/linode/token"
105106
"github.com/linode/terraform-provider-linode/v3/linode/user"
106107
"github.com/linode/terraform-provider-linode/v3/linode/users"
@@ -381,5 +382,6 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource
381382
regionvpcavailability.NewDataSource,
382383
regionsvpcavailability.NewDataSource,
383384
reservediptypes.NewDataSource,
385+
tag.NewDataSource,
384386
}
385387
}

linode/tag/datasource_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
//go:build integration || tag
2+
3+
package tag_test
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"testing"
9+
"time"
10+
11+
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
12+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
13+
"github.com/linode/linodego"
14+
"github.com/linode/terraform-provider-linode/v3/linode/acceptance"
15+
"github.com/linode/terraform-provider-linode/v3/linode/tag/tmpl"
16+
)
17+
18+
func init() {
19+
resource.AddTestSweepers("linode_tag", &resource.Sweeper{
20+
Name: "linode_tag",
21+
F: sweep,
22+
})
23+
}
24+
25+
func sweep(prefix string) error {
26+
client, err := acceptance.GetTestClient()
27+
if err != nil {
28+
return fmt.Errorf("Error getting client: %s", err)
29+
}
30+
31+
tags, err := client.ListTags(context.Background(), nil)
32+
if err != nil {
33+
return fmt.Errorf("Error getting tags: %s", err)
34+
}
35+
36+
for _, tag := range tags {
37+
if !acceptance.ShouldSweep(prefix, tag.Label) {
38+
continue
39+
}
40+
if err := client.DeleteTag(context.Background(), tag.Label); err != nil {
41+
return fmt.Errorf("Error destroying tag %q during sweep: %s", tag.Label, err)
42+
}
43+
}
44+
45+
return nil
46+
}
47+
48+
func TestAccDataSourceTag_basic(t *testing.T) {
49+
t.Parallel()
50+
51+
dsName := "data.linode_tag.test"
52+
tagLabel := acctest.RandomWithPrefix("tf_test")
53+
54+
resource.Test(t, resource.TestCase{
55+
PreCheck: func() { acceptance.PreCheck(t) },
56+
ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories,
57+
Steps: []resource.TestStep{
58+
{
59+
PreConfig: func() {
60+
client, err := acceptance.GetTestClient()
61+
if err != nil {
62+
t.Fatalf("Error getting client: %s", err)
63+
}
64+
t.Cleanup(func() {
65+
_ = client.DeleteTag(context.Background(), tagLabel)
66+
})
67+
if _, err := client.CreateTag(context.Background(), linodego.TagCreateOptions{Label: tagLabel}); err != nil {
68+
t.Fatalf("Error creating tag: %s", err)
69+
}
70+
},
71+
Config: tmpl.DataSource(t, tagLabel),
72+
Check: resource.ComposeTestCheckFunc(
73+
resource.TestCheckResourceAttr(dsName, "label", tagLabel),
74+
resource.TestCheckResourceAttr(dsName, "id", tagLabel),
75+
),
76+
},
77+
},
78+
})
79+
}
80+
81+
func TestAccDataSourceTag_reservedIP(t *testing.T) {
82+
t.Parallel()
83+
84+
dsName := "data.linode_tag.test"
85+
tagLabel := acctest.RandomWithPrefix("tf_test")
86+
var reservedIPAddress string
87+
88+
resource.Test(t, resource.TestCase{
89+
PreCheck: func() { acceptance.PreCheck(t) },
90+
ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories,
91+
Steps: []resource.TestStep{
92+
{
93+
PreConfig: func() {
94+
region, err := acceptance.GetRandomRegionWithCaps(nil, "core")
95+
if err != nil {
96+
t.Fatalf("Error finding region: %s", err)
97+
}
98+
client, err := acceptance.GetTestClient()
99+
if err != nil {
100+
t.Fatalf("Error getting client: %s", err)
101+
}
102+
ip, err := client.AllocateReserveIP(context.Background(), linodego.AllocateReserveIPOptions{
103+
Type: "ipv4",
104+
Public: true,
105+
Reserved: true,
106+
Region: region,
107+
})
108+
if err != nil {
109+
t.Fatalf("Error reserving IP: %s", err)
110+
}
111+
reservedIPAddress = ip.Address
112+
t.Cleanup(func() {
113+
_ = client.DeleteReservedIPAddress(context.Background(), ip.Address)
114+
_ = client.DeleteTag(context.Background(), tagLabel)
115+
})
116+
if _, err := client.CreateTag(context.Background(), linodego.TagCreateOptions{
117+
Label: tagLabel,
118+
ReservedIPv4Addresses: []string{ip.Address},
119+
}); err != nil {
120+
t.Fatalf("Error creating tag: %s", err)
121+
}
122+
123+
// Poll until the reserved IP association is visible in the API
124+
// before letting Terraform proceed (eventual consistency).
125+
// Skip if the API environment does not support reserved_ipv4_addresses
126+
// in POST /tags (feature may not be rolled out yet).
127+
deadline := time.Now().Add(30 * time.Second)
128+
visible := false
129+
for time.Now().Before(deadline) {
130+
objects, err := client.ListTaggedObjects(context.Background(), tagLabel, nil)
131+
if err == nil && len(objects) > 0 {
132+
visible = true
133+
break
134+
}
135+
time.Sleep(2 * time.Second)
136+
}
137+
if !visible {
138+
t.Skip("reserved_ipv4_addresses tag association not visible via API; skipping (feature may not be available in this environment)")
139+
}
140+
},
141+
Config: tmpl.DataSource(t, tagLabel),
142+
Check: resource.ComposeTestCheckFunc(
143+
resource.TestCheckResourceAttr(dsName, "label", tagLabel),
144+
resource.TestCheckResourceAttr(dsName, "id", tagLabel),
145+
resource.TestCheckResourceAttr(dsName, "objects.#", "1"),
146+
resource.TestCheckResourceAttr(dsName, "objects.0.type", "reserved_ipv4_address"),
147+
resource.TestCheckResourceAttrWith(dsName, "objects.0.id", func(val string) error {
148+
if val != reservedIPAddress {
149+
return fmt.Errorf("expected objects.0.id to be %q, got %q", reservedIPAddress, val)
150+
}
151+
return nil
152+
}),
153+
),
154+
},
155+
},
156+
})
157+
}

linode/tag/framework_datasource.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package tag
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/datasource"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
"github.com/hashicorp/terraform-plugin-log/tflog"
10+
"github.com/linode/linodego"
11+
"github.com/linode/terraform-provider-linode/v3/linode/helper"
12+
)
13+
14+
func NewDataSource() datasource.DataSource {
15+
return &DataSource{
16+
BaseDataSource: helper.NewBaseDataSource(
17+
helper.BaseDataSourceConfig{
18+
Name: "linode_tag",
19+
Schema: &frameworkDatasourceSchema,
20+
},
21+
),
22+
}
23+
}
24+
25+
type DataSource struct {
26+
helper.BaseDataSource
27+
}
28+
29+
func (d *DataSource) Read(
30+
ctx context.Context,
31+
req datasource.ReadRequest,
32+
resp *datasource.ReadResponse,
33+
) {
34+
tflog.Debug(ctx, "Read data."+d.Config.Name)
35+
36+
var data DataSourceModel
37+
38+
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
39+
if resp.Diagnostics.HasError() {
40+
return
41+
}
42+
43+
label := data.Label.ValueString()
44+
ctx = tflog.SetField(ctx, "tag_label", label)
45+
46+
objects, err := d.Meta.Client.ListTaggedObjects(ctx, label, nil)
47+
if err != nil {
48+
if linodego.IsNotFound(err) {
49+
resp.Diagnostics.AddError(
50+
fmt.Sprintf("Tag %q not found", label),
51+
err.Error(),
52+
)
53+
return
54+
}
55+
resp.Diagnostics.AddError(
56+
fmt.Sprintf("Failed to list objects for Tag %q", label),
57+
err.Error(),
58+
)
59+
return
60+
}
61+
62+
data.ID = types.StringValue(label)
63+
data.FlattenTaggedObjects(ctx, objects, &resp.Diagnostics)
64+
if resp.Diagnostics.HasError() {
65+
return
66+
}
67+
68+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
69+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package tag
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/diag"
9+
"github.com/hashicorp/terraform-plugin-framework/types"
10+
"github.com/linode/linodego"
11+
)
12+
13+
type DataSourceModel struct {
14+
ID types.String `tfsdk:"id"`
15+
Label types.String `tfsdk:"label"`
16+
Objects types.List `tfsdk:"objects"`
17+
}
18+
19+
type TaggedObjectModel struct {
20+
Type types.String `tfsdk:"type"`
21+
ID types.String `tfsdk:"id"`
22+
}
23+
24+
func (data *DataSourceModel) FlattenTaggedObjects(
25+
ctx context.Context,
26+
objects linodego.TaggedObjectList,
27+
diags *diag.Diagnostics,
28+
) {
29+
models := make([]TaggedObjectModel, 0, len(objects))
30+
31+
for _, obj := range objects {
32+
m := TaggedObjectModel{
33+
Type: types.StringValue(obj.Type),
34+
}
35+
36+
switch obj.Type {
37+
case "linode":
38+
if inst, ok := obj.Data.(linodego.Instance); ok {
39+
m.ID = types.StringValue(strconv.Itoa(inst.ID))
40+
}
41+
case "domain":
42+
if d, ok := obj.Data.(linodego.Domain); ok {
43+
m.ID = types.StringValue(strconv.Itoa(d.ID))
44+
}
45+
case "volume":
46+
if v, ok := obj.Data.(linodego.Volume); ok {
47+
m.ID = types.StringValue(strconv.Itoa(v.ID))
48+
}
49+
case "nodebalancer":
50+
if n, ok := obj.Data.(linodego.NodeBalancer); ok {
51+
m.ID = types.StringValue(strconv.Itoa(n.ID))
52+
}
53+
case "reserved_ipv4_address":
54+
if ip, ok := obj.Data.(linodego.InstanceIP); ok {
55+
m.ID = types.StringValue(ip.Address)
56+
}
57+
default:
58+
diags.AddWarning("Unknown tagged object type",
59+
fmt.Sprintf("tagged object type %q is not recognised; ID will be empty", obj.Type))
60+
m.ID = types.StringValue("")
61+
}
62+
63+
models = append(models, m)
64+
}
65+
66+
listVal, d := types.ListValueFrom(
67+
ctx,
68+
types.ObjectType{AttrTypes: tagObjectAttrTypes},
69+
models,
70+
)
71+
diags.Append(d...)
72+
if !d.HasError() {
73+
data.Objects = listVal
74+
}
75+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package tag
2+
3+
import (
4+
"github.com/hashicorp/terraform-plugin-framework/attr"
5+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
6+
"github.com/hashicorp/terraform-plugin-framework/types"
7+
)
8+
9+
var tagObjectAttrTypes = map[string]attr.Type{
10+
"type": types.StringType,
11+
"id": types.StringType,
12+
}
13+
14+
var frameworkDatasourceSchema = schema.Schema{
15+
Attributes: map[string]schema.Attribute{
16+
"id": schema.StringAttribute{
17+
Description: "The label of this Tag.",
18+
Computed: true,
19+
},
20+
"label": schema.StringAttribute{
21+
Description: "A label used to categorize resources. For display purposes only.",
22+
Required: true,
23+
},
24+
"objects": schema.ListNestedAttribute{
25+
Description: "The objects associated with this tag.",
26+
Computed: true,
27+
NestedObject: schema.NestedAttributeObject{
28+
Attributes: map[string]schema.Attribute{
29+
"type": schema.StringAttribute{
30+
Description: "The type of the tagged object " +
31+
"(e.g. linode, domain, volume, nodebalancer, reserved_ipv4_address).",
32+
Computed: true,
33+
},
34+
"id": schema.StringAttribute{
35+
Description: "The ID (or address for reserved_ipv4_address) of the tagged object.",
36+
Computed: true,
37+
},
38+
},
39+
},
40+
},
41+
},
42+
}

0 commit comments

Comments
 (0)