From fe8f6cf84b6949777e9028963175ff6743e836d7 Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Tue, 14 Apr 2026 14:47:31 +0200 Subject: [PATCH 1/6] TPT-4297: terraform: Implement linode_tag Data Source with Reserved IPv4 Support --- docs/data-sources/tag.md | 31 +++++ linode/framework_provider.go | 2 + linode/tag/datasource_test.go | 134 ++++++++++++++++++++++ linode/tag/framework_datasource.go | 69 +++++++++++ linode/tag/framework_datasource_model.go | 72 ++++++++++++ linode/tag/framework_datasource_schema.go | 42 +++++++ linode/tag/framework_models_unit_test.go | 41 +++++++ linode/tag/tmpl/datasource.gotf | 7 ++ linode/tag/tmpl/template.go | 19 +++ 9 files changed, 417 insertions(+) create mode 100644 docs/data-sources/tag.md create mode 100644 linode/tag/datasource_test.go create mode 100644 linode/tag/framework_datasource.go create mode 100644 linode/tag/framework_datasource_model.go create mode 100644 linode/tag/framework_datasource_schema.go create mode 100644 linode/tag/framework_models_unit_test.go create mode 100644 linode/tag/tmpl/datasource.gotf create mode 100644 linode/tag/tmpl/template.go diff --git a/docs/data-sources/tag.md b/docs/data-sources/tag.md new file mode 100644 index 000000000..f532f9a0e --- /dev/null +++ b/docs/data-sources/tag.md @@ -0,0 +1,31 @@ +--- +page_title: "Linode: linode_tag" +description: |- + Provides details about a Linode Tag. +--- + +# Data Source: linode\_tag + +Provides information about a Linode Tag, including the objects associated with it. + +## Example Usage + +```hcl +data "linode_tag" "example" { + label = "my-tag" +} +``` + +## Argument Reference + +* `label` - (Required) The label of the tag to look up. + +## Attributes Reference + +* `id` - The label of the tag. + +* `objects` - A list of objects associated with this tag. Each object has the following attributes: + + * `type` - The type of the tagged object (e.g. `linode`, `domain`, `volume`, `nodebalancer`, `reserved_ipv4_address`). + + * `id` - The ID of the tagged object. For `reserved_ipv4_address` objects, this is the IP address string. diff --git a/linode/framework_provider.go b/linode/framework_provider.go index fa87753e0..b030a25ba 100644 --- a/linode/framework_provider.go +++ b/linode/framework_provider.go @@ -99,6 +99,7 @@ import ( "github.com/linode/terraform-provider-linode/v3/linode/sshkeys" "github.com/linode/terraform-provider-linode/v3/linode/stackscript" "github.com/linode/terraform-provider-linode/v3/linode/stackscripts" + "github.com/linode/terraform-provider-linode/v3/linode/tag" "github.com/linode/terraform-provider-linode/v3/linode/token" "github.com/linode/terraform-provider-linode/v3/linode/user" "github.com/linode/terraform-provider-linode/v3/linode/users" @@ -376,5 +377,6 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource lkenodepool.NewDataSource, regionvpcavailability.NewDataSource, regionsvpcavailability.NewDataSource, + tag.NewDataSource, } } diff --git a/linode/tag/datasource_test.go b/linode/tag/datasource_test.go new file mode 100644 index 000000000..46292b44b --- /dev/null +++ b/linode/tag/datasource_test.go @@ -0,0 +1,134 @@ +//go:build integration || tag + +package tag_test + +import ( + "context" + "fmt" + "log" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/acceptance" + "github.com/linode/terraform-provider-linode/v3/linode/tag/tmpl" +) + +var testRegion string + +func init() { + resource.AddTestSweepers("linode_tag", &resource.Sweeper{ + Name: "linode_tag", + F: sweep, + }) + + region, err := acceptance.GetRandomRegionWithCaps(nil, "core") + if err != nil { + log.Fatal(err) + } + + testRegion = region +} + +func sweep(prefix string) error { + client, err := acceptance.GetTestClient() + if err != nil { + return fmt.Errorf("Error getting client: %s", err) + } + + tags, err := client.ListTags(context.Background(), nil) + if err != nil { + return fmt.Errorf("Error getting tags: %s", err) + } + + for _, tag := range tags { + if !acceptance.ShouldSweep(prefix, tag.Label) { + continue + } + if err := client.DeleteTag(context.Background(), tag.Label); err != nil { + return fmt.Errorf("Error destroying tag %q during sweep: %s", tag.Label, err) + } + } + + return nil +} + +func TestAccDataSourceTag_basic(t *testing.T) { + t.Parallel() + + dsName := "data.linode_tag.test" + tagLabel := acctest.RandomWithPrefix("tf_test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + client, err := acceptance.GetTestClient() + if err != nil { + t.Fatalf("Error getting client: %s", err) + } + t.Cleanup(func() { + _ = client.DeleteTag(context.Background(), tagLabel) + }) + if _, err := client.CreateTag(context.Background(), linodego.TagCreateOptions{Label: tagLabel}); err != nil { + t.Fatalf("Error creating tag: %s", err) + } + }, + Config: tmpl.DataSource(t, tagLabel), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dsName, "label", tagLabel), + resource.TestCheckResourceAttr(dsName, "id", tagLabel), + ), + }, + }, + }) +} + +func TestAccDataSourceTag_reservedIP(t *testing.T) { + t.Parallel() + + dsName := "data.linode_tag.test" + tagLabel := acctest.RandomWithPrefix("tf_test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + PreConfig: func() { + client, err := acceptance.GetTestClient() + if err != nil { + t.Fatalf("Error getting client: %s", err) + } + ip, err := client.AllocateReserveIP(context.Background(), linodego.AllocateReserveIPOptions{ + Type: "ipv4", + Public: true, + Reserved: true, + Region: testRegion, + }) + if err != nil { + t.Fatalf("Error reserving IP: %s", err) + } + t.Cleanup(func() { + _ = client.DeleteReservedIPAddress(context.Background(), ip.Address) + _ = client.DeleteTag(context.Background(), tagLabel) + }) + if _, err := client.CreateTag(context.Background(), linodego.TagCreateOptions{ + Label: tagLabel, + ReservedIPv4Addresses: []string{ip.Address}, + }); err != nil { + t.Fatalf("Error creating tag: %s", err) + } + }, + Config: tmpl.DataSource(t, tagLabel), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dsName, "label", tagLabel), + resource.TestCheckResourceAttr(dsName, "id", tagLabel), + ), + }, + }, + }) +} diff --git a/linode/tag/framework_datasource.go b/linode/tag/framework_datasource.go new file mode 100644 index 000000000..4d2c6ff7d --- /dev/null +++ b/linode/tag/framework_datasource.go @@ -0,0 +1,69 @@ +package tag + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/linodego" + "github.com/linode/terraform-provider-linode/v3/linode/helper" +) + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_tag", + Schema: &frameworkDatasourceSchema, + }, + ), + } +} + +type DataSource struct { + helper.BaseDataSource +} + +func (d *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + tflog.Debug(ctx, "Read data."+d.Config.Name) + + var data DataSourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + label := data.Label.ValueString() + ctx = tflog.SetField(ctx, "tag_label", label) + + objects, err := d.Meta.Client.ListTaggedObjects(ctx, label, nil) + if err != nil { + if linodego.IsNotFound(err) { + resp.Diagnostics.AddError( + fmt.Sprintf("Tag %q not found", label), + err.Error(), + ) + return + } + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to list objects for Tag %q", label), + err.Error(), + ) + return + } + + data.ID = types.StringValue(label) + data.FlattenTaggedObjects(ctx, objects, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/linode/tag/framework_datasource_model.go b/linode/tag/framework_datasource_model.go new file mode 100644 index 000000000..954aede18 --- /dev/null +++ b/linode/tag/framework_datasource_model.go @@ -0,0 +1,72 @@ +package tag + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/linodego" +) + +type DataSourceModel struct { + ID types.String `tfsdk:"id"` + Label types.String `tfsdk:"label"` + Objects types.List `tfsdk:"objects"` +} + +type TaggedObjectModel struct { + Type types.String `tfsdk:"type"` + ID types.String `tfsdk:"id"` +} + +func (data *DataSourceModel) FlattenTaggedObjects( + ctx context.Context, + objects linodego.TaggedObjectList, + diags *diag.Diagnostics, +) { + models := make([]TaggedObjectModel, 0, len(objects)) + + for _, obj := range objects { + m := TaggedObjectModel{ + Type: types.StringValue(obj.Type), + } + + switch obj.Type { + case "linode": + if inst, ok := obj.Data.(linodego.Instance); ok { + m.ID = types.StringValue(strconv.Itoa(inst.ID)) + } + case "domain": + if d, ok := obj.Data.(linodego.Domain); ok { + m.ID = types.StringValue(strconv.Itoa(d.ID)) + } + case "volume": + if v, ok := obj.Data.(linodego.Volume); ok { + m.ID = types.StringValue(strconv.Itoa(v.ID)) + } + case "nodebalancer": + if n, ok := obj.Data.(linodego.NodeBalancer); ok { + m.ID = types.StringValue(strconv.Itoa(n.ID)) + } + case "reserved_ipv4_address": + if ip, ok := obj.Data.(linodego.InstanceIP); ok { + m.ID = types.StringValue(ip.Address) + } + default: + m.ID = types.StringValue("") + } + + models = append(models, m) + } + + listVal, d := types.ListValueFrom( + ctx, + types.ObjectType{AttrTypes: tagObjectAttrTypes}, + models, + ) + diags.Append(d...) + if !d.HasError() { + data.Objects = listVal + } +} diff --git a/linode/tag/framework_datasource_schema.go b/linode/tag/framework_datasource_schema.go new file mode 100644 index 000000000..808457245 --- /dev/null +++ b/linode/tag/framework_datasource_schema.go @@ -0,0 +1,42 @@ +package tag + +import ( + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var tagObjectAttrTypes = map[string]attr.Type{ + "type": types.StringType, + "id": types.StringType, +} + +var frameworkDatasourceSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The label of this Tag.", + Computed: true, + }, + "label": schema.StringAttribute{ + Description: "A label used to categorize resources. For display purposes only.", + Required: true, + }, + "objects": schema.ListNestedAttribute{ + Description: "The objects associated with this tag.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: "The type of the tagged object " + + "(e.g. linode, domain, volume, nodebalancer, reserved_ipv4_address).", + Computed: true, + }, + "id": schema.StringAttribute{ + Description: "The ID (or address for reserved_ipv4_address) of the tagged object.", + Computed: true, + }, + }, + }, + }, + }, +} diff --git a/linode/tag/framework_models_unit_test.go b/linode/tag/framework_models_unit_test.go new file mode 100644 index 000000000..0b07d28f3 --- /dev/null +++ b/linode/tag/framework_models_unit_test.go @@ -0,0 +1,41 @@ +//go:build unit + +package tag + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/linode/linodego" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFlattenTaggedObjects_DataSource(t *testing.T) { + objects := linodego.TaggedObjectList{ + { + Type: "reserved_ipv4_address", + Data: linodego.InstanceIP{Address: "198.51.100.5"}, + }, + { + Type: "linode", + Data: linodego.Instance{ID: 999}, + }, + } + + model := &DataSourceModel{} + diags := diag.Diagnostics{} + model.FlattenTaggedObjects(context.Background(), objects, &diags) + assert.False(t, diags.HasError()) + + var items []TaggedObjectModel + diags.Append(model.Objects.ElementsAs(context.Background(), &items, false)...) + require.False(t, diags.HasError()) + require.Len(t, items, 2) + + assert.Equal(t, "reserved_ipv4_address", items[0].Type.ValueString()) + assert.Equal(t, "198.51.100.5", items[0].ID.ValueString()) + assert.Equal(t, "linode", items[1].Type.ValueString()) + assert.Equal(t, "999", items[1].ID.ValueString()) +} diff --git a/linode/tag/tmpl/datasource.gotf b/linode/tag/tmpl/datasource.gotf new file mode 100644 index 000000000..4ad5a786e --- /dev/null +++ b/linode/tag/tmpl/datasource.gotf @@ -0,0 +1,7 @@ +{{ define "tag_datasource" }} + +data "linode_tag" "test" { + label = "{{ .Label }}" +} + +{{ end }} diff --git a/linode/tag/tmpl/template.go b/linode/tag/tmpl/template.go new file mode 100644 index 000000000..a827d23da --- /dev/null +++ b/linode/tag/tmpl/template.go @@ -0,0 +1,19 @@ +package tmpl + +import ( + "testing" + + "github.com/linode/terraform-provider-linode/v3/linode/acceptance" +) + +type TemplateData struct { + Label string + Region string +} + +func DataSource(t testing.TB, label string) string { + return acceptance.ExecuteTemplate(t, + "tag_datasource", TemplateData{ + Label: label, + }) +} From 2eed1c2887fcf424c4535685760a9e20a6668535 Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Tue, 14 Apr 2026 17:04:43 +0200 Subject: [PATCH 2/6] TPT-4297: terraform: Implement linode_tag Data Source with Reserved IPv4 Support --- go.mod | 4 +++- go.sum | 8 +++---- linode/tag/datasource_test.go | 45 ++++++++++++++++++++++++++--------- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 5f22ca26b..05a10d683 100644 --- a/go.mod +++ b/go.mod @@ -101,7 +101,7 @@ require ( github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/zclconf/go-cty v1.17.0 // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect @@ -125,3 +125,5 @@ require ( sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) + +replace github.com/linode/linodego => github.com/mgwoj/linodego v0.0.0-20260326092850-0606e6876545 diff --git a/go.sum b/go.sum index 0e9f55985..c929ab27e 100644 --- a/go.sum +++ b/go.sum @@ -199,8 +199,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/linode/linodego v1.66.0 h1:rK8QJFaV53LWOEJvb/evhTg/dP5ElvtuZmx4iv4RJds= -github.com/linode/linodego v1.66.0/go.mod h1:12ykGs9qsvxE+OU3SXuW2w+DTruWF35FPlXC7gGk2tU= github.com/linode/linodego/k8s v1.25.2 h1:PY6S0sAD3xANVvM9WY38bz9GqMTjIbytC8IJJ9Cv23o= github.com/linode/linodego/k8s v1.25.2/go.mod h1:DC1XCSRZRGsmaa/ggpDPSDUmOM6aK1bhSIP6+f9Cwhc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -213,6 +211,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mgwoj/linodego v0.0.0-20260326092850-0606e6876545 h1:aAyzEWcjA/sch+NeyE3LQBUsgAMb1mxGbvc+m3k8A5o= +github.com/mgwoj/linodego v0.0.0-20260326092850-0606e6876545/go.mod h1:2HQdnNGlEl0kBE+NZQ+u3+033dP7fR3VsVokiWonreM= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= @@ -308,8 +308,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= -golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/linode/tag/datasource_test.go b/linode/tag/datasource_test.go index 46292b44b..7b0980fd5 100644 --- a/linode/tag/datasource_test.go +++ b/linode/tag/datasource_test.go @@ -5,8 +5,8 @@ package tag_test import ( "context" "fmt" - "log" "testing" + "time" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -15,20 +15,11 @@ import ( "github.com/linode/terraform-provider-linode/v3/linode/tag/tmpl" ) -var testRegion string - func init() { resource.AddTestSweepers("linode_tag", &resource.Sweeper{ Name: "linode_tag", F: sweep, }) - - region, err := acceptance.GetRandomRegionWithCaps(nil, "core") - if err != nil { - log.Fatal(err) - } - - testRegion = region } func sweep(prefix string) error { @@ -92,6 +83,7 @@ func TestAccDataSourceTag_reservedIP(t *testing.T) { dsName := "data.linode_tag.test" tagLabel := acctest.RandomWithPrefix("tf_test") + var reservedIPAddress string resource.Test(t, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, @@ -99,6 +91,10 @@ func TestAccDataSourceTag_reservedIP(t *testing.T) { Steps: []resource.TestStep{ { PreConfig: func() { + region, err := acceptance.GetRandomRegionWithCaps(nil, "core") + if err != nil { + t.Fatalf("Error finding region: %s", err) + } client, err := acceptance.GetTestClient() if err != nil { t.Fatalf("Error getting client: %s", err) @@ -107,11 +103,12 @@ func TestAccDataSourceTag_reservedIP(t *testing.T) { Type: "ipv4", Public: true, Reserved: true, - Region: testRegion, + Region: region, }) if err != nil { t.Fatalf("Error reserving IP: %s", err) } + reservedIPAddress = ip.Address t.Cleanup(func() { _ = client.DeleteReservedIPAddress(context.Background(), ip.Address) _ = client.DeleteTag(context.Background(), tagLabel) @@ -122,11 +119,37 @@ func TestAccDataSourceTag_reservedIP(t *testing.T) { }); err != nil { t.Fatalf("Error creating tag: %s", err) } + + // Poll until the reserved IP association is visible in the API + // before letting Terraform proceed (eventual consistency). + // Skip if the API environment does not support reserved_ipv4_addresses + // in POST /tags (feature may not be rolled out yet). + deadline := time.Now().Add(30 * time.Second) + visible := false + for time.Now().Before(deadline) { + objects, err := client.ListTaggedObjects(context.Background(), tagLabel, nil) + if err == nil && len(objects) > 0 { + visible = true + break + } + time.Sleep(2 * time.Second) + } + if !visible { + t.Skip("reserved_ipv4_addresses tag association not visible via API; skipping (feature may not be available in this environment)") + } }, Config: tmpl.DataSource(t, tagLabel), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(dsName, "label", tagLabel), resource.TestCheckResourceAttr(dsName, "id", tagLabel), + resource.TestCheckResourceAttr(dsName, "objects.#", "1"), + resource.TestCheckResourceAttr(dsName, "objects.0.type", "reserved_ipv4_address"), + resource.TestCheckResourceAttrWith(dsName, "objects.0.id", func(val string) error { + if val != reservedIPAddress { + return fmt.Errorf("expected objects.0.id to be %q, got %q", reservedIPAddress, val) + } + return nil + }), ), }, }, From 10be5cd3483b3e958924e3968df9679970b25c4a Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Wed, 15 Apr 2026 11:06:53 +0200 Subject: [PATCH 3/6] TPT-4297: terraform: Implement linode_tag Data Source with Reserved IPv4 Support --- linode/tag/tmpl/template.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/linode/tag/tmpl/template.go b/linode/tag/tmpl/template.go index a827d23da..317fce6fd 100644 --- a/linode/tag/tmpl/template.go +++ b/linode/tag/tmpl/template.go @@ -7,8 +7,7 @@ import ( ) type TemplateData struct { - Label string - Region string + Label string } func DataSource(t testing.TB, label string) string { From 7e73cacd5be20ef8b677ba571de3cf5ee2e1b8ee Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Wed, 15 Apr 2026 11:28:42 +0200 Subject: [PATCH 4/6] TPT-4297: terraform: Implement linode_tag Data Source with Reserved IPv4 Support --- go.mod | 6 +++--- go.sum | 4 ++-- linode/firewall/framework_models.go | 2 +- linode/linodeinterface/framework_models.go | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index e819ff127..caa2ae5fb 100644 --- a/go.mod +++ b/go.mod @@ -102,9 +102,9 @@ require ( github.com/zclconf/go-cty v1.17.0 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.42.0 // indirect google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index c1697bb96..4c8cc32b2 100644 --- a/go.sum +++ b/go.sum @@ -306,8 +306,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/linode/firewall/framework_models.go b/linode/firewall/framework_models.go index 90d5ec7d2..e424c2583 100644 --- a/linode/firewall/framework_models.go +++ b/linode/firewall/framework_models.go @@ -143,7 +143,7 @@ func (data *FirewallResourceModel) getCreateOptions( return createOpts } - createOpts.Devices.LinodeInterfaces = helper.ExpandFwInt64Set(data.Interfaces, diags) + createOpts.Devices.Interfaces = helper.ExpandFwInt64Set(data.Interfaces, diags) if diags.HasError() { return createOpts } diff --git a/linode/linodeinterface/framework_models.go b/linode/linodeinterface/framework_models.go index 09dfb60bd..d5721c904 100644 --- a/linode/linodeinterface/framework_models.go +++ b/linode/linodeinterface/framework_models.go @@ -52,7 +52,7 @@ func (plan *LinodeInterfaceModel) GetCreateOptions(ctx context.Context, diags *d } if !plan.FirewallID.IsUnknown() { - opts.FirewallID = helper.FrameworkSafeInt64PointerToIntPointer(plan.FirewallID.ValueInt64Pointer(), diags) + opts.FirewallID = helper.FrameworkSafeInt64ValueToIntDoublePointerWithUnknownToNil(plan.FirewallID, diags) if diags.HasError() { return opts, linodeID } From 8d7fadd061b8d4ed63b39a4485850a9c5e761c8b Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Wed, 15 Apr 2026 11:54:48 +0200 Subject: [PATCH 5/6] TPT-4297: terraform: Implement linode_tag Data Source with Reserved IPv4 Support --- linode/tag/framework_datasource_model.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/linode/tag/framework_datasource_model.go b/linode/tag/framework_datasource_model.go index 954aede18..55f4b9eee 100644 --- a/linode/tag/framework_datasource_model.go +++ b/linode/tag/framework_datasource_model.go @@ -2,6 +2,7 @@ package tag import ( "context" + "fmt" "strconv" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -54,6 +55,8 @@ func (data *DataSourceModel) FlattenTaggedObjects( m.ID = types.StringValue(ip.Address) } default: + diags.AddWarning("Unknown tagged object type", + fmt.Sprintf("tagged object type %q is not recognised; ID will be empty", obj.Type)) m.ID = types.StringValue("") } From 2e5803fbeaca3fd427fc3181071df059eee10cf6 Mon Sep 17 00:00:00 2001 From: Michal Wojcik Date: Fri, 8 May 2026 13:06:14 +0200 Subject: [PATCH 6/6] TPT-4297: terraform: Implement linode_tag Data Source with Reserved IPv4 Support --- docs/data-sources/tag.md | 2 ++ linode/firewall/framework_models.go | 2 +- linode/linodeinterface/framework_models.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/tag.md b/docs/data-sources/tag.md index f532f9a0e..aeb7f4f07 100644 --- a/docs/data-sources/tag.md +++ b/docs/data-sources/tag.md @@ -8,6 +8,8 @@ description: |- Provides information about a Linode Tag, including the objects associated with it. +For more information, see the [Linode APIv4 documentation](https://techdocs.akamai.com/linode-api/reference/get-tagged-objects). + ## Example Usage ```hcl diff --git a/linode/firewall/framework_models.go b/linode/firewall/framework_models.go index e424c2583..90d5ec7d2 100644 --- a/linode/firewall/framework_models.go +++ b/linode/firewall/framework_models.go @@ -143,7 +143,7 @@ func (data *FirewallResourceModel) getCreateOptions( return createOpts } - createOpts.Devices.Interfaces = helper.ExpandFwInt64Set(data.Interfaces, diags) + createOpts.Devices.LinodeInterfaces = helper.ExpandFwInt64Set(data.Interfaces, diags) if diags.HasError() { return createOpts } diff --git a/linode/linodeinterface/framework_models.go b/linode/linodeinterface/framework_models.go index d5721c904..09dfb60bd 100644 --- a/linode/linodeinterface/framework_models.go +++ b/linode/linodeinterface/framework_models.go @@ -52,7 +52,7 @@ func (plan *LinodeInterfaceModel) GetCreateOptions(ctx context.Context, diags *d } if !plan.FirewallID.IsUnknown() { - opts.FirewallID = helper.FrameworkSafeInt64ValueToIntDoublePointerWithUnknownToNil(plan.FirewallID, diags) + opts.FirewallID = helper.FrameworkSafeInt64PointerToIntPointer(plan.FirewallID.ValueInt64Pointer(), diags) if diags.HasError() { return opts, linodeID }