Skip to content

Commit dd4013c

Browse files
authored
feat: Onboard IaaS Public IP range datasource (#633)
* Add "stackit_public_ip_range" datasource * Add docs and example
1 parent 2cf8051 commit dd4013c

4 files changed

Lines changed: 268 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "stackit_public_ip_ranges Data Source - stackit"
4+
subcategory: ""
5+
description: |-
6+
A list of all public IP ranges that STACKIT uses.
7+
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
8+
---
9+
10+
# stackit_public_ip_ranges (Data Source)
11+
12+
A list of all public IP ranges that STACKIT uses.
13+
14+
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
15+
16+
## Example Usage
17+
18+
```terraform
19+
data "stackit_public_ip_ranges" "example" {}
20+
```
21+
22+
<!-- schema generated by tfplugindocs -->
23+
## Schema
24+
25+
### Read-Only
26+
27+
- `id` (String) Terraform's internal resource ID. It takes the values of "`public_ip_ranges.*.cidr`".
28+
- `public_ip_ranges` (Attributes List) A list of all public IP ranges. (see [below for nested schema](#nestedatt--public_ip_ranges))
29+
30+
<a id="nestedatt--public_ip_ranges"></a>
31+
### Nested Schema for `public_ip_ranges`
32+
33+
Read-Only:
34+
35+
- `cidr` (String) Classless Inter-Domain Routing (CIDR)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
data "stackit_public_ip_ranges" "example" {}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package publicipranges
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"sort"
8+
"strings"
9+
10+
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
11+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
12+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
13+
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
14+
15+
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
16+
"github.com/hashicorp/terraform-plugin-framework/attr"
17+
"github.com/hashicorp/terraform-plugin-framework/datasource"
18+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
19+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
20+
"github.com/hashicorp/terraform-plugin-framework/types"
21+
"github.com/hashicorp/terraform-plugin-log/tflog"
22+
"github.com/stackitcloud/stackit-sdk-go/core/config"
23+
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
24+
)
25+
26+
// publicIpRangesDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
27+
// This is a workaround for the lack of a global state in the provider and
28+
// needs to exist because the Configure method is called twice.
29+
var publicIpRangesDataSourceBetaCheckDone bool
30+
31+
// Ensure the implementation satisfies the expected interfaces.
32+
var (
33+
_ datasource.DataSource = &publicIpRangesDataSource{}
34+
)
35+
36+
// NewPublicIpRangesDataSource is a helper function to simplify the provider implementation.
37+
func NewPublicIpRangesDataSource() datasource.DataSource {
38+
return &publicIpRangesDataSource{}
39+
}
40+
41+
// publicIpRangesDataSource is the data source implementation.
42+
type publicIpRangesDataSource struct {
43+
client *iaas.APIClient
44+
}
45+
46+
type Model struct {
47+
Id types.String `tfsdk:"id"` // needed by TF
48+
PublicIpRanges types.List `tfsdk:"public_ip_ranges"`
49+
}
50+
51+
var publicIpRangesTypes = map[string]attr.Type{
52+
"cidr": types.StringType,
53+
}
54+
55+
// Metadata returns the data source type name.
56+
func (d *publicIpRangesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
57+
resp.TypeName = req.ProviderTypeName + "_public_ip_ranges"
58+
}
59+
60+
func (d *publicIpRangesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
61+
// Prevent panic if the provider has not been configured.
62+
if req.ProviderData == nil {
63+
return
64+
}
65+
66+
providerData, ok := req.ProviderData.(core.ProviderData)
67+
if !ok {
68+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
69+
return
70+
}
71+
72+
if !publicIpRangesDataSourceBetaCheckDone {
73+
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_public_ip_ranges", "data source")
74+
if resp.Diagnostics.HasError() {
75+
return
76+
}
77+
publicIpRangesDataSourceBetaCheckDone = true
78+
}
79+
80+
var apiClient *iaas.APIClient
81+
var err error
82+
if providerData.IaaSCustomEndpoint != "" {
83+
apiClient, err = iaas.NewAPIClient(
84+
config.WithCustomAuth(providerData.RoundTripper),
85+
config.WithEndpoint(providerData.IaaSCustomEndpoint),
86+
)
87+
} else {
88+
apiClient, err = iaas.NewAPIClient(
89+
config.WithCustomAuth(providerData.RoundTripper),
90+
config.WithRegion(providerData.Region),
91+
)
92+
}
93+
if err != nil {
94+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
95+
return
96+
}
97+
98+
d.client = apiClient
99+
tflog.Info(ctx, "iaas client configured")
100+
}
101+
102+
// Schema defines the schema for the resource.
103+
func (d *publicIpRangesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
104+
description := "A list of all public IP ranges that STACKIT uses."
105+
106+
resp.Schema = schema.Schema{
107+
MarkdownDescription: features.AddBetaDescription(description),
108+
Description: description,
109+
Attributes: map[string]schema.Attribute{
110+
"id": schema.StringAttribute{
111+
Description: "Terraform's internal resource ID. It takes the values of \"`public_ip_ranges.*.cidr`\".",
112+
Computed: true,
113+
Optional: false,
114+
},
115+
"public_ip_ranges": schema.ListNestedAttribute{
116+
Description: "A list of all public IP ranges.",
117+
Computed: true,
118+
Optional: false,
119+
Validators: []validator.List{
120+
listvalidator.ValueStringsAre(
121+
validate.CIDR(),
122+
),
123+
},
124+
NestedObject: schema.NestedAttributeObject{
125+
Attributes: map[string]schema.Attribute{
126+
"cidr": schema.StringAttribute{
127+
Description: "Classless Inter-Domain Routing (CIDR)",
128+
Computed: true,
129+
},
130+
},
131+
},
132+
},
133+
},
134+
}
135+
}
136+
137+
// Read refreshes the Terraform state with the latest data.
138+
func (d *publicIpRangesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
139+
var model Model
140+
diags := req.Config.Get(ctx, &model)
141+
resp.Diagnostics.Append(diags...)
142+
if resp.Diagnostics.HasError() {
143+
return
144+
}
145+
publicIpRangeResp, err := d.client.ListPublicIpRangesExecute(ctx)
146+
if err != nil {
147+
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
148+
if ok && oapiErr.StatusCode == http.StatusNotFound {
149+
resp.State.RemoveResource(ctx)
150+
return
151+
}
152+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP ranges", fmt.Sprintf("Calling API: %v", err))
153+
return
154+
}
155+
156+
// Map response body to schema
157+
err = mapFields(ctx, publicIpRangeResp, &model)
158+
if err != nil {
159+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP ranges", fmt.Sprintf("Processing API payload: %v", err))
160+
return
161+
}
162+
diags = resp.State.Set(ctx, model)
163+
resp.Diagnostics.Append(diags...)
164+
if resp.Diagnostics.HasError() {
165+
return
166+
}
167+
tflog.Info(ctx, "read public IP ranges")
168+
}
169+
170+
func mapFields(ctx context.Context, publicIpRangeResp *iaas.PublicNetworkListResponse, model *Model) error {
171+
if publicIpRangeResp == nil {
172+
return fmt.Errorf("response input is nil")
173+
}
174+
if model == nil {
175+
return fmt.Errorf("model input is nil")
176+
}
177+
178+
err := mapPublicIpRanges(ctx, publicIpRangeResp.Items, model)
179+
if err != nil {
180+
return fmt.Errorf("error mapping public IP ranges: %w", err)
181+
}
182+
return nil
183+
}
184+
185+
// mapPublicIpRanges map the response publicIpRanges to the model
186+
func mapPublicIpRanges(_ context.Context, publicIpRanges *[]iaas.PublicNetwork, model *Model) error {
187+
if publicIpRanges == nil {
188+
return fmt.Errorf("publicIpRanges input is nil")
189+
}
190+
if len(*publicIpRanges) == 0 {
191+
model.PublicIpRanges = types.ListNull(types.ObjectType{AttrTypes: publicIpRangesTypes})
192+
return nil
193+
}
194+
195+
var apiIpRanges []string
196+
for _, ipRange := range *publicIpRanges {
197+
if ipRange.Cidr != nil || *ipRange.Cidr != "" {
198+
apiIpRanges = append(apiIpRanges, *ipRange.Cidr)
199+
}
200+
}
201+
202+
// Sort to prevent unnecessary recreation of dependent resources due to order changes.
203+
sort.Strings(apiIpRanges)
204+
205+
modelId := strings.Join(apiIpRanges, ",")
206+
model.Id = types.StringValue(modelId)
207+
208+
var ipRangesList []attr.Value
209+
for _, cidr := range apiIpRanges {
210+
ipRangeValues := map[string]attr.Value{
211+
"cidr": types.StringValue(cidr),
212+
}
213+
ipRangeObject, diag := types.ObjectValue(publicIpRangesTypes, ipRangeValues)
214+
if diag.HasError() {
215+
return core.DiagsToError(diag)
216+
}
217+
ipRangesList = append(ipRangesList, ipRangeObject)
218+
}
219+
220+
ipRangesTF, diags := types.ListValue(
221+
types.ObjectType{AttrTypes: publicIpRangesTypes},
222+
ipRangesList,
223+
)
224+
if diags.HasError() {
225+
return core.DiagsToError(diags)
226+
}
227+
228+
model.PublicIpRanges = ipRangesTF
229+
return nil
230+
}

stackit/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
iaasNetworkInterfaceAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterfaceattach"
2525
iaasPublicIp "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicip"
2626
iaasPublicIpAssociate "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicipassociate"
27+
iaasPublicIpRanges "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicipranges"
2728
iaasSecurityGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/securitygroup"
2829
iaasSecurityGroupRule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/securitygrouprule"
2930
iaasServer "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/server"
@@ -422,6 +423,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
422423
iaasNetworkInterface.NewNetworkInterfaceDataSource,
423424
iaasVolume.NewVolumeDataSource,
424425
iaasPublicIp.NewPublicIpDataSource,
426+
iaasPublicIpRanges.NewPublicIpRangesDataSource,
425427
iaasKeyPair.NewKeyPairDataSource,
426428
iaasServer.NewServerDataSource,
427429
iaasSecurityGroup.NewSecurityGroupDataSource,

0 commit comments

Comments
 (0)