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
13 changes: 11 additions & 2 deletions docs/data-sources/ipam_next_available_subnets.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ data "bloxone_ipam_next_available_subnets" "example_tf_subs" {
cidr = 29
subnet_count = 5
}

data "bloxone_ipam_next_available_subnets" "example_next_available_subnet_by_tag" {
cidr = 30
subnet_count = 15
tag_filters = {
environment = "prd"
}
}
```

<!-- schema generated by tfplugindocs -->
Expand All @@ -35,12 +43,13 @@ data "bloxone_ipam_next_available_subnets" "example_tf_subs" {
### Required

- `cidr` (Number) The cidr value of subnets to be created.
- `id` (String) An application specific resource identity of a resource.

### Optional

- `id` (String) An application specific resource identity of a resource.
Comment thread
unasra marked this conversation as resolved.
- `subnet_count` (Number) Number of subnets to generate. Default 1 if not set.
- `tag_filters` (Map of String) Key-value pairs to filter subnets by tags.

### Read-Only

- `results` (List of String) List of Next available Subnet address in the specified resource
- `results` (List of String) List of next available subnet addresses in the specified resource.
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ data "bloxone_ipam_next_available_subnets" "example_tf_subs" {
cidr = 29
subnet_count = 5
}

data "bloxone_ipam_next_available_subnets" "example_next_available_subnet_by_tag" {
cidr = 30
subnet_count = 15
tag_filters = {
environment = "prd"
}
}
162 changes: 144 additions & 18 deletions internal/service/ipam/api_next_available_subnet_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@ package ipam
import (
"context"
"fmt"
"io"
"regexp"

"github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"

bloxoneclient "github.com/infobloxopen/bloxone-go-client/client"
"github.com/infobloxopen/bloxone-go-client/ipam"

"github.com/infobloxopen/terraform-provider-bloxone/internal/flex"
"github.com/infobloxopen/terraform-provider-bloxone/internal/utils"
)

// Ensure provider defined types fully satisfy framework interfaces.
Expand All @@ -31,10 +35,11 @@ type NextAvailableSubnetDataSource struct {
}

type IpamsvcNextAvailableSubnetModel struct {
Id types.String `tfsdk:"id"`
Cidr types.Int64 `tfsdk:"cidr"`
Count types.Int64 `tfsdk:"subnet_count"`
Results types.List `tfsdk:"results"`
Id types.String `tfsdk:"id"`
Cidr types.Int64 `tfsdk:"cidr"`
Count types.Int32 `tfsdk:"subnet_count"`
Results types.List `tfsdk:"results"`
TagFilters types.Map `tfsdk:"tag_filters"`
}

func (m *IpamsvcNextAvailableSubnetModel) FlattenResults(ctx context.Context, from []ipam.Subnet, diags *diag.Diagnostics) {
Expand All @@ -58,24 +63,34 @@ func (d *NextAvailableSubnetDataSource) Schema(ctx context.Context, req datasour
MarkdownDescription: "Retrieves the next available subnets in the specified address block.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Required: true,
Optional: true,
Computed: true,
MarkdownDescription: `An application specific resource identity of a resource.`,
Validators: []validator.String{
stringvalidator.RegexMatches(regexp.MustCompile(`^ipam/address_block/[0-9a-f-].*$`), "invalid resource ID specified"),
stringvalidator.ConflictsWith(path.MatchRoot("tag_filters")),
},
},
"cidr": schema.Int64Attribute{
Required: true,
MarkdownDescription: `The cidr value of subnets to be created.`,
},
"subnet_count": schema.Int64Attribute{
"subnet_count": schema.Int32Attribute{
Optional: true,
MarkdownDescription: `Number of subnets to generate. Default 1 if not set.`,
Validators: []validator.Int32{
int32validator.AtLeast(1),
},
},
"results": schema.ListAttribute{
ElementType: types.StringType,
Computed: true,
MarkdownDescription: "List of Next available Subnet address in the specified resource",
MarkdownDescription: "List of next available subnet addresses in the specified resource.",
},
"tag_filters": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
MarkdownDescription: "Key-value pairs to filter subnets by tags.",
},
},
}
Expand Down Expand Up @@ -110,19 +125,130 @@ func (d *NextAvailableSubnetDataSource) Read(ctx context.Context, req datasource
return
}

apiRes, _, err := d.client.IPAddressManagementAPI.
AddressBlockAPI.
ListNextAvailableSubnet(ctx, data.Id.ValueString()).
Cidr(int32(data.Cidr.ValueInt64())).
Count(int32(data.Count.ValueInt64())).
Execute()
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read AddressBlock Next Available Subnet API, got error: %s", err))
return
// Ensure subnet_count has a default value
if data.Count.IsNull() {
data.Count = types.Int32Value(1)
}
tagFilters := data.TagFilters
count := data.Count.ValueInt32()
cidrData := data.Cidr.ValueInt64()

if len(tagFilters.Elements()) > 0 {
// Find subnets by tags
tagFilterStr := flex.ExpandFrameworkMapFilterString(ctx, tagFilters, &resp.Diagnostics)

var allAddressBlocks []ipam.AddressBlock

allAddressBlocks, err := FetchAddressBlocksByTagFilter(ctx, d.client, tagFilterStr, &resp.Diagnostics)

if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Address Blocks, got error: %s", err))
return
}

if len(allAddressBlocks) == 0 {
resp.Diagnostics.AddError("No Address Blocks Found", "No address blocks found with the given tags.")
return
}

var findResults []ipam.Subnet

for _, ab := range allAddressBlocks {
if *ab.Cidr >= cidrData {
continue
}
findResultsLen := int32(len(findResults))
if findResultsLen >= count {
break
}

remainingCount := count - findResultsLen
findResult, findErr := d.findSubnet(ctx, *ab.Id, int32(cidrData), remainingCount)
if findErr != nil {
// Check if the error contains relevant information about available blocks
errorBody := []byte(findErr.Error())
availableCount := utils.ExtractAvailableCountFromError(errorBody)
if availableCount > 0 {
// Retry with the available count
partialResult, retryErr := d.findSubnet(ctx, *ab.Id, int32(cidrData), availableCount)
if retryErr != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Error finding subnet after retry: %s", retryErr))
return
}
findResults = append(findResults, partialResult...)
} else {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Next Available Subnet, got error: %s", findErr))
return
}
continue
}

if len(findResult) > 0 {
findResults = append(findResults, findResult...)
}
}
finalResultsCount := int32(len(findResults))
if finalResultsCount < count {
resp.Diagnostics.AddError(
"Insufficient Available Subnets",
fmt.Sprintf("Requested %d subnets with CIDR %d, but only %d were found. Not enough subnets available across all checked address blocks.", count, cidrData, finalResultsCount),
)
return
}
data.FlattenResults(ctx, findResults, &resp.Diagnostics)
} else {
// Use original next available address logic to find by ID
apiRes, _, err := d.client.IPAddressManagementAPI.
AddressBlockAPI.
ListNextAvailableSubnet(ctx, data.Id.ValueString()).
Cidr(int32(cidrData)).
Count(count).
Execute()
if err != nil {
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Next Available Subnet API, got error: %s", err))
return
}

data.FlattenResults(ctx, apiRes.GetResults(), &resp.Diagnostics)
}

data.FlattenResults(ctx, apiRes.GetResults(), &resp.Diagnostics)

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

// Helper function to find subnets by ID and count
func (d *NextAvailableSubnetDataSource) findSubnet(ctx context.Context, id string, cidr int32, count int32) ([]ipam.Subnet, error) {
Comment thread
Tejashree-RS marked this conversation as resolved.
apiRes, httpRes, err := d.client.IPAddressManagementAPI.AddressBlockAPI.
ListNextAvailableSubnet(ctx, id).
Cidr(cidr).
Count(count).
Execute()
if err != nil {
// Check for 400 status code without relying on specific error type

if httpRes != nil && httpRes.StatusCode == 400 {
// Convert response body to string if it's available from httpRes
bodyBytes, _ := io.ReadAll(httpRes.Body)
errMsg := httpRes.Body.Close() // Close the body after reading
if errMsg != nil {
return nil, errMsg
}
// Try to extract available count
availableCount := utils.ExtractAvailableCountFromError(bodyBytes)
if availableCount > 0 {
// Retry with the available count
retryRes, _, retryErr := d.client.IPAddressManagementAPI.AddressBlockAPI.
ListNextAvailableSubnet(ctx, id).
Cidr(cidr).
Count(availableCount).
Execute()
if retryErr == nil {
return retryRes.GetResults(), nil
}
}
}
return nil, err
}

return apiRes.GetResults(), nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (
"github.com/infobloxopen/terraform-provider-bloxone/internal/acctest"
)

var envTag = acctest.RandomNameWithPrefix("prd")
var locTag = acctest.RandomNameWithPrefix("data-center-1")

func TestDataSourceNextAvailableSubnet(t *testing.T) {
dataSourceName := "data.bloxone_ipam_next_available_subnets.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
Expand Down Expand Up @@ -43,6 +45,23 @@ func TestDataSourceNextAvailableSubnet(t *testing.T) {
resource.TestCheckResourceAttrSet(dataSourceName, "results.2"),
),
},
{
Config: testAccDataSourceNextAvailableSubnetWithSingleTagFilter(24, 2),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, "results.#", "2"),
resource.TestCheckResourceAttrSet(dataSourceName, "results.0"),
resource.TestCheckResourceAttrSet(dataSourceName, "results.1"),
),
},
{
Config: testAccDataSourceNextAvailableSubnetWithMultipleTagFilters(24, 3),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(dataSourceName, "results.#", "3"),
resource.TestCheckResourceAttrSet(dataSourceName, "results.0"),
resource.TestCheckResourceAttrSet(dataSourceName, "results.1"),
resource.TestCheckResourceAttrSet(dataSourceName, "results.2"),
),
},
},
})
}
Expand All @@ -60,6 +79,7 @@ func testAccDataSourceNextAvailableSubnetBaseConfig() string {
}
`, acctest.RandomNameWithPrefix("nextAvailableIPSpace"), acctest.RandomNameWithPrefix("nextAvailableAB"))
}

func testAccDataSourceNextAvailableSubnet(count, cidr int) string {
var config string
if count == 1 {
Expand All @@ -79,3 +99,73 @@ func testAccDataSourceNextAvailableSubnet(count, cidr int) string {

return strings.Join([]string{testAccDataSourceNextAvailableSubnetBaseConfig(), config}, "")
}

// testAccDataSourceNextAvailableSubnetWithSingleTagFilter creates test configuration for next available subnet with a single tag filter
func testAccDataSourceNextAvailableSubnetWithSingleTagFilter(cidr, count int) string {
config := fmt.Sprintf(`
data "bloxone_ipam_next_available_subnets" "test" {
cidr = %d
subnet_count = %d
tag_filters = {
environment = %q
}
depends_on = [
"bloxone_ipam_address_block.test_next_available_by_single_tag",
"bloxone_ipam_address_block.test_next_available_by_mulitple_tags",
]
}`, cidr, count, envTag)

return strings.Join([]string{testAccDataSourceNextAvailableSubnetWithTagsBaseConfig(), config}, "")
}

// testAccDataSourceNextAvailableSubnetWithMultipleTagFilters creates test configuration for next available subnet with multiple tag filters
func testAccDataSourceNextAvailableSubnetWithMultipleTagFilters(cidr, count int) string {
config := fmt.Sprintf(`
data "bloxone_ipam_next_available_subnets" "test" {
cidr = %d
subnet_count = %d
tag_filters = {
environment = %q
location = %q
}
depends_on = [
"bloxone_ipam_address_block.test_next_available_by_mulitple_tags",
]
}`, cidr, count, envTag, locTag)

return strings.Join([]string{testAccDataSourceNextAvailableSubnetWithTagsBaseConfig(), config}, "")
}

// testAccDataSourceNextAvailableSubnetWithTagsBaseConfig creates base resources with tags for testing
func testAccDataSourceNextAvailableSubnetWithTagsBaseConfig() string {
space := acctest.RandomNameWithPrefix("IPSpace")
config := fmt.Sprintf(`

resource "bloxone_ipam_address_block" "test_next_available_by_id" {
address = "192.168.0.0"
cidr = 16
space = bloxone_ipam_ip_space.test.id
}

resource "bloxone_ipam_address_block" "test_next_available_by_mulitple_tags" {
address = "13.0.0.0"
cidr = 16
space = bloxone_ipam_ip_space.test.id
tags = {
environment = %q
location = %q
}
}

resource "bloxone_ipam_address_block" "test_next_available_by_single_tag" {
address = "10.0.0.0"
cidr = 16
space = bloxone_ipam_ip_space.test.id
tags = {
environment = %q
}
}
`, envTag, locTag, envTag)

return strings.Join([]string{testAccBaseWithIPSpace(space), config}, "")
}
Loading