Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/data-sources/tag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
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.
Comment thread
mgwoj marked this conversation as resolved.

For more information, see the [Linode APIv4 documentation](https://techdocs.akamai.com/linode-api/reference/get-tagged-objects).

## 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.
2 changes: 2 additions & 0 deletions linode/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,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"
Expand Down Expand Up @@ -381,5 +382,6 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource
regionvpcavailability.NewDataSource,
regionsvpcavailability.NewDataSource,
reservediptypes.NewDataSource,
tag.NewDataSource,
}
}
157 changes: 157 additions & 0 deletions linode/tag/datasource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//go:build integration || tag

package tag_test

import (
"context"
"fmt"
"testing"
"time"

"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"
)

func init() {
resource.AddTestSweepers("linode_tag", &resource.Sweeper{
Name: "linode_tag",
F: sweep,
})
}

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")
var reservedIPAddress string

resource.Test(t, resource.TestCase{
PreCheck: func() { acceptance.PreCheck(t) },
ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories,
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)
}
ip, err := client.AllocateReserveIP(context.Background(), linodego.AllocateReserveIPOptions{
Type: "ipv4",
Public: true,
Reserved: true,
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)
})
if _, err := client.CreateTag(context.Background(), linodego.TagCreateOptions{
Label: tagLabel,
ReservedIPv4Addresses: []string{ip.Address},
}); err != nil {
t.Fatalf("Error creating tag: %s", err)
}
Comment thread
mgwoj marked this conversation as resolved.

// 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
}),
),
Comment thread
mgwoj marked this conversation as resolved.
},
},
})
}
69 changes: 69 additions & 0 deletions linode/tag/framework_datasource.go
Original file line number Diff line number Diff line change
@@ -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)...)
}
75 changes: 75 additions & 0 deletions linode/tag/framework_datasource_model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package tag

import (
"context"
"fmt"
"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))
}
Comment thread
mgwoj marked this conversation as resolved.
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:
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("")
}
Comment thread
mgwoj marked this conversation as resolved.

models = append(models, m)
}

listVal, d := types.ListValueFrom(
ctx,
types.ObjectType{AttrTypes: tagObjectAttrTypes},
models,
)
diags.Append(d...)
if !d.HasError() {
data.Objects = listVal
}
}
42 changes: 42 additions & 0 deletions linode/tag/framework_datasource_schema.go
Original file line number Diff line number Diff line change
@@ -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,
},
},
},
},
},
}
Loading
Loading