Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terraform Support for Managing Reserved IP Addresses #1598

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ require (
github.com/hashicorp/terraform-plugin-mux v0.16.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0
github.com/hashicorp/terraform-plugin-testing v1.10.0
github.com/linode/linodego v1.41.0
github.com/linode/linodego v1.41.1-0.20240925173015-b20be2e986e0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's wait for next linodego release

github.com/linode/linodego/k8s v1.25.2
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.27.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ 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.41.0 h1:GcP7JIBr9iLRJ9FwAtb9/WCT1DuPJS/xUApapfdjtiY=
github.com/linode/linodego v1.41.0/go.mod h1:Ow4/XZ0yvWBzt3iAHwchvhSx30AyLintsSMvvQ2/SJY=
github.com/linode/linodego v1.41.1-0.20240925173015-b20be2e986e0 h1:FbA+CGk47kdAm2XmVEm1rVCLFlo98uJ+5jnbTLjVkv8=
github.com/linode/linodego v1.41.1-0.20240925173015-b20be2e986e0/go.mod h1:Ow4/XZ0yvWBzt3iAHwchvhSx30AyLintsSMvvQ2/SJY=
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=
Expand Down
3 changes: 3 additions & 0 deletions linode/framework_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package linode
import (
"context"

"github.com/linode/terraform-provider-linode/v2/linode/networkreservedips"
"github.com/linode/terraform-provider-linode/v2/linode/vpcips"

"github.com/hashicorp/terraform-plugin-framework/datasource"
Expand Down Expand Up @@ -222,6 +223,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res
firewall.NewResource,
placementgroup.NewResource,
placementgroupassignment.NewResource,
networkreservedips.NewResource,
}
}

Expand Down Expand Up @@ -286,5 +288,6 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource
placementgroups.NewDataSource,
childaccount.NewDataSource,
childaccounts.NewDataSource,
networkreservedips.NewDataSource,
}
}
59 changes: 59 additions & 0 deletions linode/networkreservedips/datasource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//go:build integration || networkreservedips

package networkreservedips_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/linode/terraform-provider-linode/v2/linode/acceptance"
"github.com/linode/terraform-provider-linode/v2/linode/networkreservedips/tmpl"
)

func TestAccDataSource_reservedIP(t *testing.T) {
t.Parallel()

resourceName := "data.linode_reserved_ip.test"
region, _ := acceptance.GetRandomRegionWithCaps([]string{"linodes"}, "core")

resource.Test(t, resource.TestCase{
PreCheck: func() { acceptance.PreCheck(t) },
ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: tmpl.DataBasic(t, region),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "address"),
resource.TestCheckResourceAttrSet(resourceName, "region"),
resource.TestCheckResourceAttrSet(resourceName, "gateway"),
resource.TestCheckResourceAttrSet(resourceName, "subnet_mask"),
resource.TestCheckResourceAttrSet(resourceName, "prefix"),
resource.TestCheckResourceAttrSet(resourceName, "type"),
resource.TestCheckResourceAttrSet(resourceName, "public"),
resource.TestCheckResourceAttrSet(resourceName, "rdns"),
resource.TestCheckResourceAttrSet(resourceName, "linode_id"),
resource.TestCheckResourceAttrSet(resourceName, "reserved"),
),
},
},
})
}

func TestAccDataSource_reservedIPList(t *testing.T) {
t.Parallel()

resourceName := "data.linode_reserved_ip.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { acceptance.PreCheck(t) },
ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories,
Steps: []resource.TestStep{
{
Config: tmpl.DataList(t),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "reserved_ips.#"),
),
},
},
})
}
124 changes: 124 additions & 0 deletions linode/networkreservedips/framework_datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package networkreservedips

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/v2/linode/helper"
)

func NewDataSource() datasource.DataSource {
return &DataSource{
BaseDataSource: helper.NewBaseDataSource(
helper.BaseDataSourceConfig{
Name: "linode_reserved_ip",
Schema: &frameworkDataSourceSchema,
},
),
}
}

type DataSource struct {
helper.BaseDataSource
}

type DataSourceModel struct {
ID types.String `tfsdk:"id"`
Address types.String `tfsdk:"address"`
Region types.String `tfsdk:"region"`
Gateway types.String `tfsdk:"gateway"`
SubnetMask types.String `tfsdk:"subnet_mask"`
Prefix types.Int64 `tfsdk:"prefix"`
Type types.String `tfsdk:"type"`
Public types.Bool `tfsdk:"public"`
RDNS types.String `tfsdk:"rdns"`
LinodeID types.Int64 `tfsdk:"linode_id"`
Reserved types.Bool `tfsdk:"reserved"`
ReservedIPs types.List `tfsdk:"reserved_ips"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may need to build another data source for listing reserved IPs, instead of having it here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I will be having a new datasource for this

}

func (data *DataSourceModel) parseIP(ip *linodego.InstanceIP) {
data.ID = types.StringValue(ip.Address)
data.Address = types.StringValue(ip.Address)
data.Region = types.StringValue(ip.Region)
data.Gateway = types.StringValue(ip.Gateway)
data.SubnetMask = types.StringValue(ip.SubnetMask)
data.Prefix = types.Int64Value(int64(ip.Prefix))
data.Type = types.StringValue(string(ip.Type))
data.Public = types.BoolValue(ip.Public)
data.RDNS = types.StringValue(ip.RDNS)
data.LinodeID = types.Int64Value(int64(ip.LinodeID))
data.Reserved = types.BoolValue(ip.Reserved)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this data source model and parsing function to framwork_models to keep the data source short and clear?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I will be doing it the next commit


func (d *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
tflog.Debug(ctx, "Read data.linode_reserved_ip")

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

if !data.Address.IsNull() {
// Fetch a specific reserved IP
ip, err := d.Meta.Client.GetReservedIPAddress(ctx, data.Address.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Unable to get Reserved IP Address",
err.Error(),
)
return
}
data.parseIP(ip)
} else {
// List all reserved IPs
filter := ""
if !data.Region.IsNull() {
filter = fmt.Sprintf("{\"region\":\"%s\"}", data.Region.ValueString())
}
ips, err := d.Meta.Client.ListReservedIPAddresses(ctx, &linodego.ListOptions{Filter: filter})
if err != nil {
resp.Diagnostics.AddError(
"Unable to list Reserved IP Addresses",
err.Error(),
)
return
}

reservedIPs := make([]ReservedIPObject, len(ips))
for i, ip := range ips {
reservedIPs[i] = ReservedIPObject{
ID: types.StringValue(ip.Address),
Address: types.StringValue(ip.Address),
Region: types.StringValue(ip.Region),
Gateway: types.StringValue(ip.Gateway),
SubnetMask: types.StringValue(ip.SubnetMask),
Prefix: types.Int64Value(int64(ip.Prefix)),
Type: types.StringValue(string(ip.Type)),
Public: types.BoolValue(ip.Public),
RDNS: types.StringValue(ip.RDNS),
LinodeID: types.Int64Value(int64(ip.LinodeID)),
Reserved: types.BoolValue(ip.Reserved),
}
}

reservedIPsValue, diags := types.ListValueFrom(ctx, reservedIPObjectType, reservedIPs)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.ReservedIPs = reservedIPsValue

// If there are IPs, populate the first one's details for backwards compatibility
if len(ips) > 0 {
data.parseIP(&ips[0])
}
}

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
91 changes: 91 additions & 0 deletions linode/networkreservedips/framework_datasource_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package networkreservedips

import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type ReservedIPObject struct {
ID types.String `tfsdk:"id"`
Address types.String `tfsdk:"address"`
Region types.String `tfsdk:"region"`
Gateway types.String `tfsdk:"gateway"`
SubnetMask types.String `tfsdk:"subnet_mask"`
Prefix types.Int64 `tfsdk:"prefix"`
Type types.String `tfsdk:"type"`
Public types.Bool `tfsdk:"public"`
RDNS types.String `tfsdk:"rdns"`
LinodeID types.Int64 `tfsdk:"linode_id"`
Reserved types.Bool `tfsdk:"reserved"`
}

var reservedIPObjectType = types.ObjectType{
AttrTypes: map[string]attr.Type{
"id": types.StringType,
"address": types.StringType,
"region": types.StringType,
"gateway": types.StringType,
"subnet_mask": types.StringType,
"prefix": types.Int64Type,
"type": types.StringType,
"public": types.BoolType,
"rdns": types.StringType,
"linode_id": types.Int64Type,
"reserved": types.BoolType,
},
}

var frameworkDataSourceSchema = schema.Schema{
Attributes: map[string]schema.Attribute{
"region": schema.StringAttribute{
Description: "The Region in which to reserve the IP address.",
Optional: true,
},
"address": schema.StringAttribute{
Description: "The reserved IP address.",
Optional: true,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understand the endpoint correctly, but it looks like address is a required attribute to get this data source?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made it optional since I am implementing both fetch and list endpoints in the same datasource. But now it seems like it is indeed a better idea to separate the functionalities into individual datasources. I will be doing that in the next commit.

"gateway": schema.StringAttribute{
Description: "The default gateway for this address.",
Computed: true,
},
"subnet_mask": schema.StringAttribute{
Description: "The mask that separates host bits from network bits for this address.",
Computed: true,
},
"prefix": schema.Int64Attribute{
Description: "The number of bits set in the subnet mask.",
Computed: true,
},
"type": schema.StringAttribute{
Description: "The type of address this is (ipv4, ipv6, ipv6/pool, ipv6/range).",
Computed: true,
},
"public": schema.BoolAttribute{
Description: "Whether this is a public or private IP address.",
Computed: true,
},
"rdns": schema.StringAttribute{
Description: "The reverse DNS assigned to this address.",
Computed: true,
},
"linode_id": schema.Int64Attribute{
Description: "The ID of the Linode this address currently belongs to.",
Computed: true,
},
"reserved": schema.BoolAttribute{
Description: "Whether this IP is reserved or not.",
Computed: true,
},
"id": schema.StringAttribute{
Description: "The unique ID of the reserved IP address.",
Computed: true,
},
"reserved_ips": schema.ListAttribute{
Description: "A list of all reserved IPs.",
Computed: true,
ElementType: reservedIPObjectType,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I mentioned above, we can remove this list to its own data source.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah will be separating it into a new datasource

},
}
69 changes: 69 additions & 0 deletions linode/networkreservedips/framework_models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package networkreservedips

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/linode/linodego"

"github.com/linode/terraform-provider-linode/v2/linode/helper"
)

type ReservedIPModel struct {
ID types.String `tfsdk:"id"`
Region types.String `tfsdk:"region"`
Address types.String `tfsdk:"address"`
Gateway types.String `tfsdk:"gateway"`
SubnetMask types.String `tfsdk:"subnet_mask"`
Prefix types.Int64 `tfsdk:"prefix"`
Type types.String `tfsdk:"type"`
Public types.Bool `tfsdk:"public"`
RDNS types.String `tfsdk:"rdns"`
LinodeID types.Int64 `tfsdk:"linode_id"`
Reserved types.Bool `tfsdk:"reserved"`
}

func (m *ReservedIPModel) FlattenReservedIP(
ctx context.Context,
ip linodego.InstanceIP,
preserveKnown bool,
) diag.Diagnostics {
var diags diag.Diagnostics
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like no diags is raised from this function. You can just remove it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into it. I will remove it if it is not being triggered.


m.ID = helper.KeepOrUpdateString(m.ID, ip.Address, preserveKnown)
m.Region = helper.KeepOrUpdateString(m.Region, ip.Region, preserveKnown)
m.Address = helper.KeepOrUpdateString(m.Address, ip.Address, preserveKnown)
m.Gateway = helper.KeepOrUpdateString(m.Gateway, ip.Gateway, preserveKnown)
m.SubnetMask = helper.KeepOrUpdateString(m.SubnetMask, ip.SubnetMask, preserveKnown)
m.Prefix = helper.KeepOrUpdateInt64(m.Prefix, int64(ip.Prefix), preserveKnown)
m.Type = helper.KeepOrUpdateString(m.Type, string(ip.Type), preserveKnown)
m.Public = helper.KeepOrUpdateBool(m.Public, ip.Public, preserveKnown)
m.RDNS = helper.KeepOrUpdateString(m.RDNS, ip.RDNS, preserveKnown)
m.LinodeID = helper.KeepOrUpdateInt64(m.LinodeID, int64(ip.LinodeID), preserveKnown)
m.Reserved = helper.KeepOrUpdateBool(m.Reserved, ip.Reserved, preserveKnown)

return diags
}

func (m *ReservedIPModel) CopyFrom(
ctx context.Context,
other ReservedIPModel,
preserveKnown bool,
) diag.Diagnostics {
var diags diag.Diagnostics

m.ID = helper.KeepOrUpdateValue(m.ID, other.ID, preserveKnown)
m.Region = helper.KeepOrUpdateValue(m.Region, other.Region, preserveKnown)
m.Address = helper.KeepOrUpdateValue(m.Address, other.Address, preserveKnown)
m.Gateway = helper.KeepOrUpdateValue(m.Gateway, other.Gateway, preserveKnown)
m.SubnetMask = helper.KeepOrUpdateValue(m.SubnetMask, other.SubnetMask, preserveKnown)
m.Prefix = helper.KeepOrUpdateValue(m.Prefix, other.Prefix, preserveKnown)
m.Type = helper.KeepOrUpdateValue(m.Type, other.Type, preserveKnown)
m.Public = helper.KeepOrUpdateValue(m.Public, other.Public, preserveKnown)
m.RDNS = helper.KeepOrUpdateValue(m.RDNS, other.RDNS, preserveKnown)
m.LinodeID = helper.KeepOrUpdateValue(m.LinodeID, other.LinodeID, preserveKnown)
m.Reserved = helper.KeepOrUpdateValue(m.Reserved, other.Reserved, preserveKnown)

return diags
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove the CopyFrom function since this resource is not updatable and it's not used.

Loading