diff --git a/linode/framework_provider.go b/linode/framework_provider.go index 7dc261184..25d5f03a4 100644 --- a/linode/framework_provider.go +++ b/linode/framework_provider.go @@ -53,6 +53,8 @@ import ( "github.com/linode/terraform-provider-linode/v2/linode/nbs" "github.com/linode/terraform-provider-linode/v2/linode/nbtypes" "github.com/linode/terraform-provider-linode/v2/linode/networkingip" + "github.com/linode/terraform-provider-linode/v2/linode/networkreservedip" + "github.com/linode/terraform-provider-linode/v2/linode/networkreservedips" "github.com/linode/terraform-provider-linode/v2/linode/networktransferprices" "github.com/linode/terraform-provider-linode/v2/linode/objbucket" "github.com/linode/terraform-provider-linode/v2/linode/objcluster" @@ -225,6 +227,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res firewall.NewResource, placementgroup.NewResource, placementgroupassignment.NewResource, + networkreservedip.NewResource, } } @@ -293,5 +296,7 @@ func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource placementgroups.NewDataSource, childaccount.NewDataSource, childaccounts.NewDataSource, + networkreservedip.NewDataSourceFetch, + networkreservedips.NewDataSourceList, } } diff --git a/linode/image/resource_test.go b/linode/image/resource_test.go index 8184c5d7b..f40af614b 100644 --- a/linode/image/resource_test.go +++ b/linode/image/resource_test.go @@ -36,8 +36,10 @@ var testImageBytesNew = []byte{ 0xe4, 0x02, 0x00, 0x7a, 0x7a, 0x6f, 0xed, 0x03, 0x00, 0x00, 0x00, } -var testRegion string -var testRegions []string +var ( + testRegion string + testRegions []string +) func init() { resource.AddTestSweepers("linode_image", &resource.Sweeper{ diff --git a/linode/networkreservedip/datasource_fetch_model.go b/linode/networkreservedip/datasource_fetch_model.go new file mode 100644 index 000000000..cb6d9197e --- /dev/null +++ b/linode/networkreservedip/datasource_fetch_model.go @@ -0,0 +1,56 @@ +package networkreservedip + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/linode/linodego" +) + +type DataSourceFetchModel 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"` +} + +func (data *DataSourceFetchModel) 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) +} + +// 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 +// } diff --git a/linode/networkreservedip/datasource_fetch_schema.go b/linode/networkreservedip/datasource_fetch_schema.go new file mode 100644 index 000000000..efc1e58dd --- /dev/null +++ b/linode/networkreservedip/datasource_fetch_schema.go @@ -0,0 +1,87 @@ +package networkreservedip + +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 frameworkDataSourceFetchSchema = 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.", + Computed: true, + Optional: true, + }, + "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, + }, + }, +} diff --git a/linode/networkreservedip/datasource_test.go b/linode/networkreservedip/datasource_test.go new file mode 100644 index 000000000..5520d9fa0 --- /dev/null +++ b/linode/networkreservedip/datasource_test.go @@ -0,0 +1,40 @@ +//go:build integration || networkreservedip + +package networkreservedip_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/networkreservedip/tmpl" +) + +func TestAccDataSource_reservedIP(t *testing.T) { + t.Parallel() + + resourceName := "data.linode_reserved_ip_fetch.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"), + ), + }, + }, + }) +} diff --git a/linode/networkreservedip/framework_datasource_fetch.go b/linode/networkreservedip/framework_datasource_fetch.go new file mode 100644 index 000000000..59ff5f163 --- /dev/null +++ b/linode/networkreservedip/framework_datasource_fetch.go @@ -0,0 +1,90 @@ +package networkreservedip + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/terraform-provider-linode/v2/linode/helper" +) + +func NewDataSourceFetch() datasource.DataSource { + return &DataSource{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_reserved_ip_fetch", + Schema: &frameworkDataSourceFetchSchema, + }, + ), + } +} + +type DataSource struct { + helper.BaseDataSource +} + +func (d *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "Read data.linode_reserved_ip") + + var data DataSourceFetchModel + 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 := "" + // 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)...) +} diff --git a/linode/networkreservedip/framework_models.go b/linode/networkreservedip/framework_models.go new file mode 100644 index 000000000..9d4accabc --- /dev/null +++ b/linode/networkreservedip/framework_models.go @@ -0,0 +1,69 @@ +package networkreservedip + +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 + + 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 +// } diff --git a/linode/networkreservedip/framework_resource.go b/linode/networkreservedip/framework_resource.go new file mode 100644 index 000000000..cf85923e6 --- /dev/null +++ b/linode/networkreservedip/framework_resource.go @@ -0,0 +1,159 @@ +package networkreservedip + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "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 NewResource() resource.Resource { + return &Resource{ + BaseResource: helper.NewBaseResource( + helper.BaseResourceConfig{ + Name: "linode_reserved_ip", + IDType: types.StringType, + Schema: &frameworkResourceSchema, + }, + ), + } +} + +type Resource struct { + helper.BaseResource +} + +func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + tflog.Debug(ctx, "Starting Create for linode_reserved_ip") + var data ReservedIPModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + ctx = populateLogAttributes(ctx, &data) + + client := r.Meta.Client + reserveIP, err := client.ReserveIPAddress(ctx, linodego.ReserveIPOptions{ + Region: data.Region.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Failed to reserve IP address", + err.Error(), + ) + return + } + + if reserveIP == nil { + resp.Diagnostics.AddError("nil Pointer", "received nil pointer of the reserved ip") + return + } + + data.ID = types.StringValue(reserveIP.Address) + tflog.Debug(ctx, "Setting ID for reserved IP", map[string]interface{}{ + "id": data.ID.ValueString(), + }) + + diags := data.FlattenReservedIP(ctx, *reserveIP, true) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Debug(ctx, "Read linode_reserved_ip") + var data ReservedIPModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + if helper.FrameworkAttemptRemoveResourceForEmptyID(ctx, data.ID, resp) { + return + } + + ctx = populateLogAttributes(ctx, &data) + + client := r.Meta.Client + address := data.Address.ValueString() + + reservedIP, err := client.GetReservedIPAddress(ctx, address) + if err != nil { + if lerr, ok := err.(*linodego.Error); ok && lerr.Code == 404 { + resp.Diagnostics.AddWarning( + "Reserved IP No Longer Exists", + fmt.Sprintf( + "Removing reserved IP %s from state because it no longer exists", + data.ID.ValueString(), + ), + ) + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError( + "Unable to Refresh the Reserved IP", + fmt.Sprintf( + "Error finding the specified Reserved IP: %s", + err.Error(), + ), + ) + return + } + + // if reservedIP == nil { + // resp.Diagnostics.AddError("nil Pointer", "received nil pointer of the reserved ip") + // return + // } + + resp.Diagnostics.Append(data.FlattenReservedIP(ctx, *reservedIP, false)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Reserved IPs cannot be updated, so this method is left empty +} + +func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state ReservedIPModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + client := r.Meta.Client + address := state.Address.ValueString() + + tflog.Debug(ctx, "client.DeleteReservedIPAddress(...)") + if err := client.DeleteReservedIPAddress(ctx, address); err != nil { + if lErr, ok := err.(*linodego.Error); (ok && lErr.Code != 404) || !ok { + resp.Diagnostics.AddError( + "Failed to Delete Reserved IP", + fmt.Sprintf( + "failed to delete reserved ip (%s): %s", + address, err.Error(), + ), + ) + } + } +} + +func populateLogAttributes(ctx context.Context, data *ReservedIPModel) context.Context { + return helper.SetLogFieldBulk(ctx, map[string]any{ + "region": data.Region.ValueString(), + "address": data.ID.ValueString(), + }) +} diff --git a/linode/networkreservedip/framework_schema.go b/linode/networkreservedip/framework_schema.go new file mode 100644 index 000000000..7ab010669 --- /dev/null +++ b/linode/networkreservedip/framework_schema.go @@ -0,0 +1,60 @@ +package networkreservedip + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +var frameworkResourceSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "region": schema.StringAttribute{ + Description: "The Region in which to reserve the IP address.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "address": schema.StringAttribute{ + Description: "The reserved IP address.", + Computed: true, + }, + "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. For public IPv4 addresses, this will be set to " + + "a default value provided by Linode if not explicitly set.", + 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, + }, + }, +} diff --git a/linode/networkreservedip/resource_test.go b/linode/networkreservedip/resource_test.go new file mode 100644 index 000000000..3ec18081b --- /dev/null +++ b/linode/networkreservedip/resource_test.go @@ -0,0 +1,54 @@ +//go:build integration || networkreservedip + +package networkreservedip_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" + "github.com/linode/terraform-provider-linode/v2/linode/networkreservedip/tmpl" +) + +var testRegion string + +func init() { + region, err := acceptance.GetRandomRegionWithCaps([]string{"linodes"}, "core") + if err != nil { + panic(fmt.Sprintf("Error getting random region: %s", err)) + } + testRegion = region + fmt.Println(testRegion) +} + +func TestAccResource_reserveIP(t *testing.T) { + t.Parallel() + + resName := "linode_reserved_ip.test" + instanceName := acctest.RandomWithPrefix("tf_test") + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acceptance.PreCheck(t) + }, + ProtoV5ProviderFactories: acceptance.ProtoV5ProviderFactories, + CheckDestroy: acceptance.CheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: tmpl.ReserveIP(t, instanceName, testRegion), + Check: resource.ComposeTestCheckFunc( + func(s *terraform.State) error { + return nil + }, + resource.TestCheckResourceAttrSet(resName, "id"), + resource.TestCheckResourceAttrSet(resName, "address"), + resource.TestCheckResourceAttr(resName, "region", testRegion), + ), + }, + }, + }) + + t.Log("Finished TestAccResource_reserveIP") +} diff --git a/linode/networkreservedip/tmpl/reserved_ip.gotf b/linode/networkreservedip/tmpl/reserved_ip.gotf new file mode 100644 index 000000000..efc2ff330 --- /dev/null +++ b/linode/networkreservedip/tmpl/reserved_ip.gotf @@ -0,0 +1,12 @@ +{{ define "reserved_ip_data_basic" }} + +resource "linode_reserved_ip" "test" { + region = "{{ .Region }}" +} + +data "linode_reserved_ip_fetch" "test" { + address = linode_reserved_ip.test.address +} + +{{ end }} + diff --git a/linode/networkreservedip/tmpl/reserved_ip_basic.gotf b/linode/networkreservedip/tmpl/reserved_ip_basic.gotf new file mode 100644 index 000000000..23e617dcf --- /dev/null +++ b/linode/networkreservedip/tmpl/reserved_ip_basic.gotf @@ -0,0 +1,7 @@ +{{ define "reserved_ip_basic" }} + +resource "linode_reserved_ip" "test" { + region = "{{ .Region }}" +} + +{{ end }} \ No newline at end of file diff --git a/linode/networkreservedip/tmpl/template.go b/linode/networkreservedip/tmpl/template.go new file mode 100644 index 000000000..80f1f70d9 --- /dev/null +++ b/linode/networkreservedip/tmpl/template.go @@ -0,0 +1,27 @@ +package tmpl + +import ( + "testing" + + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" +) + +type TemplateData struct { + Region string + Address string +} + +func DataBasic(t *testing.T, region string) string { + return acceptance.ExecuteTemplate(t, + "reserved_ip_data_basic", TemplateData{ + Region: region, + }) +} + +// ReserveIP generates the Terraform configuration for reserving an IP address +func ReserveIP(t *testing.T, name, region string) string { + return acceptance.ExecuteTemplate(t, + "reserved_ip_basic", TemplateData{ + Region: region, + }) +} diff --git a/linode/networkreservedips/datasource_list_model.go b/linode/networkreservedips/datasource_list_model.go new file mode 100644 index 000000000..79656ad51 --- /dev/null +++ b/linode/networkreservedips/datasource_list_model.go @@ -0,0 +1,9 @@ +package networkreservedips + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type DataSourceListModel struct { + ReservedIPs types.List `tfsdk:"reserved_ips"` +} diff --git a/linode/networkreservedips/datasource_list_schema.go b/linode/networkreservedips/datasource_list_schema.go new file mode 100644 index 000000000..7d8d38d81 --- /dev/null +++ b/linode/networkreservedips/datasource_list_schema.go @@ -0,0 +1,47 @@ +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 frameworkDataSourceListSchema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "reserved_ips": schema.ListAttribute{ + Description: "A list of all reserved IPs.", + Computed: true, + ElementType: reservedIPObjectType, + }, + }, +} diff --git a/linode/networkreservedips/datasource_list_test.go b/linode/networkreservedips/datasource_list_test.go new file mode 100644 index 000000000..97cfe8052 --- /dev/null +++ b/linode/networkreservedips/datasource_list_test.go @@ -0,0 +1,30 @@ +//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_reservedIPList(t *testing.T) { + t.Parallel() + + resourceName := "data.linode_reserved_ip_list.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.#"), + ), + }, + }, + }) +} diff --git a/linode/networkreservedips/framework_datasource_list.go b/linode/networkreservedips/framework_datasource_list.go new file mode 100644 index 000000000..2320d3119 --- /dev/null +++ b/linode/networkreservedips/framework_datasource_list.go @@ -0,0 +1,72 @@ +package networkreservedips + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/linode/terraform-provider-linode/v2/linode/helper" +) + +func NewDataSourceList() datasource.DataSource { + return &DataSourceList{ + BaseDataSource: helper.NewBaseDataSource( + helper.BaseDataSourceConfig{ + Name: "linode_reserved_ip_list", + Schema: &frameworkDataSourceListSchema, + }, + ), + } +} + +type DataSourceList struct { + helper.BaseDataSource +} + +func (d *DataSourceList) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "Read data.linode_reserved_ip_list") + + var data DataSourceListModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + client := d.Meta.Client + + // List all reserved IPs + ips, err := client.ListReservedIPAddresses(ctx, nil) + 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(true), // All IPs in this list are reserved + } + } + + reservedIPsValue, diags := types.ListValueFrom(ctx, reservedIPObjectType, reservedIPs) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.ReservedIPs = reservedIPsValue + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/linode/networkreservedips/tmpl/reserved_ip_list.gotf b/linode/networkreservedips/tmpl/reserved_ip_list.gotf new file mode 100644 index 000000000..e2357dc13 --- /dev/null +++ b/linode/networkreservedips/tmpl/reserved_ip_list.gotf @@ -0,0 +1,9 @@ +{{ define "reserved_ip_data_list" }} + +data "linode_reserved_ip_list" "test" {} + +output "reserved_ips" { + value = data.linode_reserved_ip_list.test.reserved_ips +} + +{{ end }} \ No newline at end of file diff --git a/linode/networkreservedips/tmpl/template.go b/linode/networkreservedips/tmpl/template.go new file mode 100644 index 000000000..68826c4ea --- /dev/null +++ b/linode/networkreservedips/tmpl/template.go @@ -0,0 +1,11 @@ +package tmpl + +import ( + "testing" + + "github.com/linode/terraform-provider-linode/v2/linode/acceptance" +) + +func DataList(t *testing.T) string { + return acceptance.ExecuteTemplate(t, "reserved_ip_data_list", nil) +}