Merge branch 'stackitcloud:main' into alpha

This commit is contained in:
Marcel S. Henselin 2025-12-17 16:09:23 +01:00 committed by GitHub
commit c07c81b091
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
125 changed files with 8395 additions and 6044 deletions

View file

@ -33,16 +33,18 @@ func NewAffinityGroupDatasource() datasource.DataSource {
}
type affinityGroupDatasource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
func (d *affinityGroupDatasource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -61,7 +63,7 @@ func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaR
MarkdownDescription: descriptionMain,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`affinity_group_id`\".",
Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`affinity_group_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -72,6 +74,11 @@ func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaR
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"affinity_group_id": schema.StringAttribute{
Description: "The affinity group ID.",
Required: true,
@ -117,14 +124,16 @@ func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadR
return
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
affinityGroupId := model.AffinityGroupId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId)
affinityGroupResp, err := d.client.GetAffinityGroupExecute(ctx, projectId, affinityGroupId)
affinityGroupResp, err := d.client.GetAffinityGroupExecute(ctx, projectId, region, affinityGroupId)
if err != nil {
utils.LogError(
ctx,
@ -142,7 +151,7 @@ func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadR
ctx = core.LogResponse(ctx)
err = mapFields(ctx, affinityGroupResp, &model)
err = mapFields(ctx, affinityGroupResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Processing API payload: %v", err))
}

View file

@ -17,7 +17,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@ -33,12 +32,14 @@ var (
_ resource.Resource = &affinityGroupResource{}
_ resource.ResourceWithConfigure = &affinityGroupResource{}
_ resource.ResourceWithImportState = &affinityGroupResource{}
_ resource.ResourceWithModifyPlan = &affinityGroupResource{}
)
// Model is the provider's internal model
type Model struct {
Id types.String `tfsdk:"id"`
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
AffinityGroupId types.String `tfsdk:"affinity_group_id"`
Name types.String `tfsdk:"name"`
Policy types.String `tfsdk:"policy"`
@ -51,7 +52,8 @@ func NewAffinityGroupResource() resource.Resource {
// affinityGroupResource is the resource implementation.
type affinityGroupResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -59,14 +61,45 @@ func (r *affinityGroupResource) Metadata(_ context.Context, req resource.Metadat
resp.TypeName = req.ProviderTypeName + "_affinity_group"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *affinityGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *affinityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -75,13 +108,13 @@ func (r *affinityGroupResource) Configure(ctx context.Context, req resource.Conf
}
func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Affinity Group schema. Must have a `region` specified in the provider configuration."
description := "Affinity Group schema."
resp.Schema = schema.Schema{
Description: description,
MarkdownDescription: description + "\n\n" + exampleUsageWithServer + policies,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`affinity_group_id`\".",
Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`affinity_group_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -98,6 +131,15 @@ func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaReque
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"affinity_group_id": schema.StringAttribute{
Description: "The affinity group ID.",
Computed: true,
@ -153,19 +195,21 @@ func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateR
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
// Create new affinityGroup
payload, err := toCreatePayload(&model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Creating API payload: %v", err))
return
}
affinityGroupResp, err := r.client.CreateAffinityGroup(ctx, projectId).CreateAffinityGroupPayload(*payload).Execute()
affinityGroupResp, err := r.client.CreateAffinityGroup(ctx, projectId, region).CreateAffinityGroupPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Calling API: %v", err))
return
@ -176,7 +220,7 @@ func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateR
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupResp.Id)
// Map response body to schema
err = mapFields(ctx, affinityGroupResp, &model)
err = mapFields(ctx, affinityGroupResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Processing API payload: %v", err))
return
@ -199,14 +243,16 @@ func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadReque
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
affinityGroupId := model.AffinityGroupId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId)
affinityGroupResp, err := r.client.GetAffinityGroupExecute(ctx, projectId, affinityGroupId)
affinityGroupResp, err := r.client.GetAffinityGroupExecute(ctx, projectId, region, affinityGroupId)
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -219,7 +265,7 @@ func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadReque
ctx = core.LogResponse(ctx)
err = mapFields(ctx, affinityGroupResp, &model)
err = mapFields(ctx, affinityGroupResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Processing API payload: %v", err))
}
@ -247,15 +293,17 @@ func (r *affinityGroupResource) Delete(ctx context.Context, req resource.DeleteR
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
affinityGroupId := model.AffinityGroupId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId)
// Delete existing affinity group
err := r.client.DeleteAffinityGroupExecute(ctx, projectId, affinityGroupId)
err := r.client.DeleteAffinityGroupExecute(ctx, projectId, region, affinityGroupId)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting affinity group", fmt.Sprintf("Calling API: %v", err))
return
@ -269,21 +317,20 @@ func (r *affinityGroupResource) Delete(ctx context.Context, req resource.DeleteR
func (r *affinityGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing affinity group",
fmt.Sprintf("Expected import indentifier with format: [project_id],[affinity_group_id], got: %q", req.ID),
fmt.Sprintf("Expected import indentifier with format: [project_id],[region],[affinity_group_id], got: %q", req.ID),
)
return
}
projectId := idParts[0]
affinityGroupId := idParts[1]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"affinity_group_id": idParts[2],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("affinity_group_id"), affinityGroupId)...)
tflog.Info(ctx, "affinity group state imported")
}
@ -301,7 +348,7 @@ func toCreatePayload(model *Model) (*iaas.CreateAffinityGroupPayload, error) {
}, nil
}
func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model *Model) error {
func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model *Model, region string) error {
if affinityGroupResp == nil {
return fmt.Errorf("response input is nil")
}
@ -319,7 +366,8 @@ func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model
return fmt.Errorf("affinity group id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), affinityGroupId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, affinityGroupId)
model.Region = types.StringValue(region)
if affinityGroupResp.Members != nil && len(*affinityGroupResp.Members) > 0 {
members, diags := types.ListValueFrom(ctx, types.StringType, *affinityGroupResp.Members)

View file

@ -11,52 +11,56 @@ import (
)
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.AffinityGroup
region string
}
tests := []struct {
description string
state Model
input *iaas.AffinityGroup
args args
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
AffinityGroupId: types.StringValue("aid"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
AffinityGroupId: types.StringValue("aid"),
},
input: &iaas.AffinityGroup{
Id: utils.Ptr("aid"),
},
region: "eu01",
},
&iaas.AffinityGroup{
Id: utils.Ptr("aid"),
},
Model{
Id: types.StringValue("pid,aid"),
expected: Model{
Id: types.StringValue("pid,eu01,aid"),
ProjectId: types.StringValue("pid"),
AffinityGroupId: types.StringValue("aid"),
Name: types.StringNull(),
Policy: types.StringNull(),
Members: types.ListNull(types.StringType),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_affinity_group_id",
Model{
ProjectId: types.StringValue("pid"),
description: "no_affinity_group_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.AffinityGroup{},
},
&iaas.AffinityGroup{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -64,7 +68,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed")
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %v", diff)
}

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,7 @@ var (
type DataSourceModel struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
ImageId types.String `tfsdk:"image_id"`
Name types.String `tfsdk:"name"`
DiskFormat types.String `tfsdk:"disk_format"`
@ -49,7 +50,8 @@ func NewImageDataSource() datasource.DataSource {
// imageDataSource is the data source implementation.
type imageDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -58,12 +60,13 @@ func (d *imageDataSource) Metadata(_ context.Context, req datasource.MetadataReq
}
func (d *imageDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -72,14 +75,14 @@ func (d *imageDataSource) Configure(ctx context.Context, req datasource.Configur
}
// Schema defines the schema for the datasource.
func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (d *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "Image datasource schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: description,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -90,6 +93,11 @@ func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"image_id": schema.StringAttribute{
Description: "The image ID.",
Required: true,
@ -203,23 +211,26 @@ func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
}
}
// // Read refreshes the Terraform state with the latest data.
func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Read refreshes the Terraform state with the latest data.
func (d *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model DataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
imageId := model.ImageId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "image_id", imageId)
imageResp, err := r.client.GetImage(ctx, projectId, imageId).Execute()
imageResp, err := d.client.GetImage(ctx, projectId, region, imageId).Execute()
if err != nil {
utils.LogError(
ctx,
@ -238,7 +249,7 @@ func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest,
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapDataSourceFields(ctx, imageResp, &model)
err = mapDataSourceFields(ctx, imageResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err))
return
@ -252,7 +263,7 @@ func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest,
tflog.Info(ctx, "image read")
}
func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel) error {
func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel, region string) error {
if imageResp == nil {
return fmt.Errorf("response input is nil")
}
@ -269,7 +280,8 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data
return fmt.Errorf("image id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId)
model.Region = types.StringValue(region)
// Map config
var configModel = &configModel{}

View file

@ -12,69 +12,81 @@ import (
)
func TestMapDataSourceFields(t *testing.T) {
type args struct {
state DataSourceModel
input *iaas.Image
region string
}
tests := []struct {
description string
state DataSourceModel
input *iaas.Image
args args
expected DataSourceModel
isValid bool
}{
{
"default_values",
DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
description: "default_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
DataSourceModel{
Id: types.StringValue("pid,iid"),
expected: DataSourceModel{
Id: types.StringValue("pid,eu01,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
description: "simple_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Region: types.StringValue("eu01"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
Name: utils.Ptr("name"),
DiskFormat: utils.Ptr("format"),
MinDiskSize: utils.Ptr(int64(1)),
MinRam: utils.Ptr(int64(1)),
Protected: utils.Ptr(true),
Scope: utils.Ptr("scope"),
Config: &iaas.ImageConfig{
BootMenu: utils.Ptr(true),
CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
NicModel: iaas.NewNullableString(utils.Ptr("model")),
OperatingSystem: utils.Ptr("os"),
OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
SecureBoot: utils.Ptr(true),
Uefi: utils.Ptr(true),
VideoModel: iaas.NewNullableString(utils.Ptr("model")),
VirtioScsi: utils.Ptr(true),
},
Checksum: &iaas.ImageChecksum{
Algorithm: utils.Ptr("algorithm"),
Digest: utils.Ptr("digest"),
},
Labels: &map[string]interface{}{
"key": "value",
},
},
region: "eu02",
},
&iaas.Image{
Id: utils.Ptr("iid"),
Name: utils.Ptr("name"),
DiskFormat: utils.Ptr("format"),
MinDiskSize: utils.Ptr(int64(1)),
MinRam: utils.Ptr(int64(1)),
Protected: utils.Ptr(true),
Scope: utils.Ptr("scope"),
Config: &iaas.ImageConfig{
BootMenu: utils.Ptr(true),
CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
NicModel: iaas.NewNullableString(utils.Ptr("model")),
OperatingSystem: utils.Ptr("os"),
OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
SecureBoot: utils.Ptr(true),
Uefi: utils.Ptr(true),
VideoModel: iaas.NewNullableString(utils.Ptr("model")),
VirtioScsi: utils.Ptr(true),
},
Checksum: &iaas.ImageChecksum{
Algorithm: utils.Ptr("algorithm"),
Digest: utils.Ptr("digest"),
},
Labels: &map[string]interface{}{
"key": "value",
},
},
DataSourceModel{
Id: types.StringValue("pid,iid"),
expected: DataSourceModel{
Id: types.StringValue("pid,eu02,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Name: types.StringValue("name"),
@ -105,47 +117,50 @@ func TestMapDataSourceFields(t *testing.T) {
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"empty_labels",
DataSourceModel{
description: "empty_labels",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
expected: DataSourceModel{
Id: types.StringValue("pid,eu01,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
Region: types.StringValue("eu01"),
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
DataSourceModel{
Id: types.StringValue("pid,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
true,
isValid: true,
},
{
"response_nil_fail",
DataSourceModel{},
nil,
DataSourceModel{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
DataSourceModel{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
},
input: &iaas.Image{},
},
&iaas.Image{},
DataSourceModel{},
false,
expected: DataSourceModel{},
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapDataSourceFields(context.Background(), tt.input, &tt.state)
err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -153,7 +168,7 @@ func TestMapDataSourceFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -15,7 +15,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
@ -40,11 +39,13 @@ var (
_ resource.Resource = &imageResource{}
_ resource.ResourceWithConfigure = &imageResource{}
_ resource.ResourceWithImportState = &imageResource{}
_ resource.ResourceWithModifyPlan = &imageResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
ImageId types.String `tfsdk:"image_id"`
Name types.String `tfsdk:"name"`
DiskFormat types.String `tfsdk:"disk_format"`
@ -111,7 +112,8 @@ func NewImageResource() resource.Resource {
// imageResource is the resource implementation.
type imageResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -119,14 +121,45 @@ func (r *imageResource) Metadata(_ context.Context, req resource.MetadataRequest
resp.TypeName = req.ProviderTypeName + "_image"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *imageResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *imageResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -140,7 +173,7 @@ func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp
Description: "Image resource schema. Must have a `region` specified in the provider configuration.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -157,6 +190,15 @@ func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"image_id": schema.StringAttribute{
Description: "The image ID.",
Computed: true,
@ -378,11 +420,12 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest,
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
@ -391,7 +434,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest,
}
// Create new image
imageCreateResp, err := r.client.CreateImage(ctx, projectId).CreateImagePayload(*payload).Execute()
imageCreateResp, err := r.client.CreateImage(ctx, projectId, region).CreateImagePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err))
return
@ -401,15 +444,15 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest,
ctx = tflog.SetField(ctx, "image_id", *imageCreateResp.Id)
// Get the image object, as the create response does not contain all fields
image, err := r.client.GetImage(ctx, projectId, *imageCreateResp.Id).Execute()
// Get the image object, as the creation response does not contain all fields
image, err := r.client.GetImage(ctx, projectId, region, *imageCreateResp.Id).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err))
return
}
// Map response body to schema
err = mapFields(ctx, image, &model)
err = mapFields(ctx, image, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Processing API payload: %v", err))
return
@ -430,7 +473,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest,
}
// Wait for image to become available
waiter := wait.UploadImageWaitHandler(ctx, r.client, projectId, *imageCreateResp.Id)
waiter := wait.UploadImageWaitHandler(ctx, r.client, projectId, region, *imageCreateResp.Id)
waiter = waiter.SetTimeout(7 * 24 * time.Hour) // Set timeout to one week, to make the timeout useless
waitResp, err := waiter.WaitWithContext(ctx)
if err != nil {
@ -439,7 +482,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest,
}
// Map response body to schema
err = mapFields(ctx, waitResp, &model)
err = mapFields(ctx, waitResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Processing API payload: %v", err))
return
@ -454,7 +497,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest,
tflog.Info(ctx, "Image created")
}
// // Read refreshes the Terraform state with the latest data.
// Read refreshes the Terraform state with the latest data.
func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
@ -462,15 +505,18 @@ func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
imageId := model.ImageId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "image_id", imageId)
imageResp, err := r.client.GetImage(ctx, projectId, imageId).Execute()
imageResp, err := r.client.GetImage(ctx, projectId, region, imageId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -484,7 +530,7 @@ func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, imageResp, &model)
err = mapFields(ctx, imageResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err))
return
@ -507,12 +553,15 @@ func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest,
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
imageId := model.ImageId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "image_id", imageId)
// Retrieve values from state
@ -530,7 +579,7 @@ func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest,
return
}
// Update existing image
updatedImage, err := r.client.UpdateImage(ctx, projectId, imageId).UpdateImagePayload(*payload).Execute()
updatedImage, err := r.client.UpdateImage(ctx, projectId, region, imageId).UpdateImagePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Calling API: %v", err))
return
@ -538,7 +587,7 @@ func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest,
ctx = core.LogResponse(ctx)
err = mapFields(ctx, updatedImage, &model)
err = mapFields(ctx, updatedImage, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Processing API payload: %v", err))
return
@ -563,14 +612,15 @@ func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest,
projectId := model.ProjectId.ValueString()
imageId := model.ImageId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "image_id", imageId)
ctx = tflog.SetField(ctx, "region", region)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "image_id", imageId)
// Delete existing image
err := r.client.DeleteImage(ctx, projectId, imageId).Execute()
err := r.client.DeleteImage(ctx, projectId, region, imageId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("Calling API: %v", err))
return
@ -578,7 +628,7 @@ func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest,
ctx = core.LogResponse(ctx)
_, err = wait.DeleteImageWaitHandler(ctx, r.client, projectId, imageId).WaitWithContext(ctx)
_, err = wait.DeleteImageWaitHandler(ctx, r.client, projectId, region, imageId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("image deletion waiting: %v", err))
return
@ -588,29 +638,28 @@ func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,image_id
// The expected format of the resource import identifier is: project_id,region,image_id
func (r *imageResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing image",
fmt.Sprintf("Expected import identifier with format: [project_id],[image_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[image_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
imageId := idParts[1]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "image_id", imageId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"image_id": idParts[2],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("image_id"), imageId)...)
tflog.Info(ctx, "Image state imported")
}
func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error {
func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model, region string) error {
if imageResp == nil {
return fmt.Errorf("response input is nil")
}
@ -627,7 +676,8 @@ func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error {
return fmt.Errorf("image id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId)
model.Region = types.StringValue(region)
// Map config
var configModel = &configModel{}

View file

@ -17,69 +17,81 @@ import (
)
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.Image
region string
}
tests := []struct {
description string
state Model
input *iaas.Image
args args
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
Model{
Id: types.StringValue("pid,iid"),
expected: Model{
Id: types.StringValue("pid,eu01,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
Model{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Region: types.StringValue("eu01"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
Name: utils.Ptr("name"),
DiskFormat: utils.Ptr("format"),
MinDiskSize: utils.Ptr(int64(1)),
MinRam: utils.Ptr(int64(1)),
Protected: utils.Ptr(true),
Scope: utils.Ptr("scope"),
Config: &iaas.ImageConfig{
BootMenu: utils.Ptr(true),
CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
NicModel: iaas.NewNullableString(utils.Ptr("model")),
OperatingSystem: utils.Ptr("os"),
OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
SecureBoot: utils.Ptr(true),
Uefi: utils.Ptr(true),
VideoModel: iaas.NewNullableString(utils.Ptr("model")),
VirtioScsi: utils.Ptr(true),
},
Checksum: &iaas.ImageChecksum{
Algorithm: utils.Ptr("algorithm"),
Digest: utils.Ptr("digest"),
},
Labels: &map[string]interface{}{
"key": "value",
},
},
region: "eu02",
},
&iaas.Image{
Id: utils.Ptr("iid"),
Name: utils.Ptr("name"),
DiskFormat: utils.Ptr("format"),
MinDiskSize: utils.Ptr(int64(1)),
MinRam: utils.Ptr(int64(1)),
Protected: utils.Ptr(true),
Scope: utils.Ptr("scope"),
Config: &iaas.ImageConfig{
BootMenu: utils.Ptr(true),
CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
NicModel: iaas.NewNullableString(utils.Ptr("model")),
OperatingSystem: utils.Ptr("os"),
OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
SecureBoot: utils.Ptr(true),
Uefi: utils.Ptr(true),
VideoModel: iaas.NewNullableString(utils.Ptr("model")),
VirtioScsi: utils.Ptr(true),
},
Checksum: &iaas.ImageChecksum{
Algorithm: utils.Ptr("algorithm"),
Digest: utils.Ptr("digest"),
},
Labels: &map[string]interface{}{
"key": "value",
},
},
Model{
Id: types.StringValue("pid,iid"),
expected: Model{
Id: types.StringValue("pid,eu02,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Name: types.StringValue("name"),
@ -110,47 +122,48 @@ func TestMapFields(t *testing.T) {
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"empty_labels",
Model{
description: "empty_labels",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
expected: Model{
Id: types.StringValue("pid,eu01,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
Region: types.StringValue("eu01"),
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
Model{
Id: types.StringValue("pid,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.Image{},
},
&iaas.Image{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -158,7 +171,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -36,6 +36,7 @@ var (
type DataSourceModel struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
ImageId types.String `tfsdk:"image_id"`
Name types.String `tfsdk:"name"`
NameRegex types.String `tfsdk:"name_regex"`
@ -113,7 +114,8 @@ func NewImageV2DataSource() datasource.DataSource {
// imageDataV2Source is the data source implementation.
type imageDataV2Source struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -122,17 +124,18 @@ func (d *imageDataV2Source) Metadata(_ context.Context, req datasource.MetadataR
}
func (d *imageDataV2Source) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_image_v2", "datasource")
features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_image_v2", "datasource")
if resp.Diagnostics.HasError() {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -189,7 +192,7 @@ func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -200,6 +203,11 @@ func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"image_id": schema.StringAttribute{
Description: "Image ID to fetch directly",
Optional: true,
@ -357,6 +365,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest
}
projectID := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
imageID := model.ImageId.ValueString()
name := model.Name.ValueString()
nameRegex := model.NameRegex.ValueString()
@ -373,6 +382,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectID)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "image_id", imageID)
ctx = tflog.SetField(ctx, "name", name)
ctx = tflog.SetField(ctx, "name_regex", nameRegex)
@ -383,7 +393,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest
// Case 1: Direct lookup by image ID
if imageID != "" {
imageResp, err = d.client.GetImage(ctx, projectID, imageID).Execute()
imageResp, err = d.client.GetImage(ctx, projectID, region, imageID).Execute()
if err != nil {
utils.LogError(ctx, &resp.Diagnostics, err, "Reading image",
fmt.Sprintf("Image with ID %q does not exist in project %q.", imageID, projectID),
@ -409,7 +419,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest
}
// Fetch all available images
imageList, err := d.client.ListImages(ctx, projectID).Execute()
imageList, err := d.client.ListImages(ctx, projectID, region).Execute()
if err != nil {
utils.LogError(ctx, &resp.Diagnostics, err, "List images", "Unable to fetch images", nil)
return
@ -457,7 +467,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest
imageResp = filteredImages[0]
}
err = mapDataSourceFields(ctx, imageResp, &model)
err = mapDataSourceFields(ctx, imageResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err))
return
@ -473,7 +483,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest
tflog.Info(ctx, "image read")
}
func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel) error {
func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel, region string) error {
if imageResp == nil {
return fmt.Errorf("response input is nil")
}
@ -490,7 +500,8 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data
return fmt.Errorf("image id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId)
model.Region = types.StringValue(region)
// Map config
var configModel = &configModel{}

View file

@ -12,69 +12,81 @@ import (
)
func TestMapDataSourceFields(t *testing.T) {
type args struct {
state DataSourceModel
input *iaas.Image
region string
}
tests := []struct {
description string
state DataSourceModel
input *iaas.Image
args args
expected DataSourceModel
isValid bool
}{
{
"default_values",
DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
description: "default_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
DataSourceModel{
Id: types.StringValue("pid,iid"),
expected: DataSourceModel{
Id: types.StringValue("pid,eu01,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
description: "simple_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Region: types.StringValue("eu01"),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
Name: utils.Ptr("name"),
DiskFormat: utils.Ptr("format"),
MinDiskSize: utils.Ptr(int64(1)),
MinRam: utils.Ptr(int64(1)),
Protected: utils.Ptr(true),
Scope: utils.Ptr("scope"),
Config: &iaas.ImageConfig{
BootMenu: utils.Ptr(true),
CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
NicModel: iaas.NewNullableString(utils.Ptr("model")),
OperatingSystem: utils.Ptr("os"),
OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
SecureBoot: utils.Ptr(true),
Uefi: utils.Ptr(true),
VideoModel: iaas.NewNullableString(utils.Ptr("model")),
VirtioScsi: utils.Ptr(true),
},
Checksum: &iaas.ImageChecksum{
Algorithm: utils.Ptr("algorithm"),
Digest: utils.Ptr("digest"),
},
Labels: &map[string]interface{}{
"key": "value",
},
},
region: "eu02",
},
&iaas.Image{
Id: utils.Ptr("iid"),
Name: utils.Ptr("name"),
DiskFormat: utils.Ptr("format"),
MinDiskSize: utils.Ptr(int64(1)),
MinRam: utils.Ptr(int64(1)),
Protected: utils.Ptr(true),
Scope: utils.Ptr("scope"),
Config: &iaas.ImageConfig{
BootMenu: utils.Ptr(true),
CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")),
DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")),
NicModel: iaas.NewNullableString(utils.Ptr("model")),
OperatingSystem: utils.Ptr("os"),
OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")),
OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")),
RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")),
RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")),
SecureBoot: utils.Ptr(true),
Uefi: utils.Ptr(true),
VideoModel: iaas.NewNullableString(utils.Ptr("model")),
VirtioScsi: utils.Ptr(true),
},
Checksum: &iaas.ImageChecksum{
Algorithm: utils.Ptr("algorithm"),
Digest: utils.Ptr("digest"),
},
Labels: &map[string]interface{}{
"key": "value",
},
},
DataSourceModel{
Id: types.StringValue("pid,iid"),
expected: DataSourceModel{
Id: types.StringValue("pid,eu02,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Name: types.StringValue("name"),
@ -105,47 +117,48 @@ func TestMapDataSourceFields(t *testing.T) {
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"empty_labels",
DataSourceModel{
description: "empty_labels",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
input: &iaas.Image{
Id: utils.Ptr("iid"),
},
region: "eu01",
},
expected: DataSourceModel{
Id: types.StringValue("pid,eu01,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
Region: types.StringValue("eu01"),
},
&iaas.Image{
Id: utils.Ptr("iid"),
},
DataSourceModel{
Id: types.StringValue("pid,iid"),
ProjectId: types.StringValue("pid"),
ImageId: types.StringValue("iid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
true,
isValid: true,
},
{
"response_nil_fail",
DataSourceModel{},
nil,
DataSourceModel{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
DataSourceModel{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
},
input: &iaas.Image{},
},
&iaas.Image{},
DataSourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapDataSourceFields(context.Background(), tt.input, &tt.state)
err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -153,7 +166,7 @@ func TestMapDataSourceFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -21,7 +21,7 @@ var (
_ datasource.DataSource = &keyPairDataSource{}
)
// NewVolumeDataSource is a helper function to simplify the provider implementation.
// NewKeyPairDataSource is a helper function to simplify the provider implementation.
func NewKeyPairDataSource() datasource.DataSource {
return &keyPairDataSource{}
}
@ -51,7 +51,7 @@ func (d *keyPairDataSource) Configure(ctx context.Context, req datasource.Config
}
// Schema defines the schema for the resource.
func (r *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (d *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "Key pair resource schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
@ -84,7 +84,7 @@ func (r *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
}
// Read refreshes the Terraform state with the latest data.
func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
func (d *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -97,7 +97,7 @@ func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest
ctx = tflog.SetField(ctx, "name", name)
keypairResp, err := r.client.GetKeyPair(ctx, name).Execute()
keypairResp, err := d.client.GetKeyPair(ctx, name).Execute()
if err != nil {
utils.LogError(
ctx,

View file

@ -7,10 +7,12 @@ import (
"sort"
"strings"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
@ -19,7 +21,6 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// Ensure the implementation satisfies the expected interfaces.
@ -28,6 +29,7 @@ var _ datasource.DataSource = &machineTypeDataSource{}
type DataSourceModel struct {
Id types.String `tfsdk:"id"` // required by Terraform to identify state
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
SortAscending types.Bool `tfsdk:"sort_ascending"`
Filter types.String `tfsdk:"filter"`
Description types.String `tfsdk:"description"`
@ -44,7 +46,8 @@ func NewMachineTypeDataSource() datasource.DataSource {
}
type machineTypeDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
@ -52,17 +55,18 @@ func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.Metad
}
func (d *machineTypeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_machine_type", "datasource")
features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_machine_type", "datasource")
if resp.Diagnostics.HasError() {
return
}
client := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
client := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -76,7 +80,7 @@ func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaReq
MarkdownDescription: features.AddBetaDescription("Machine type data source.", core.Datasource),
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -87,6 +91,11 @@ func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaReq
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"sort_ascending": schema.BoolAttribute{
Description: "Sort machine types by name ascending (`true`) or descending (`false`). Defaults to `false`",
Optional: true,
@ -142,15 +151,17 @@ func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
sortAscending := model.SortAscending.ValueBool()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "filter_is_null", model.Filter.IsNull())
ctx = tflog.SetField(ctx, "filter_is_unknown", model.Filter.IsUnknown())
listMachineTypeReq := d.client.ListMachineTypes(ctx, projectId)
listMachineTypeReq := d.client.ListMachineTypes(ctx, projectId, region)
if !model.Filter.IsNull() && !model.Filter.IsUnknown() && strings.TrimSpace(model.Filter.ValueString()) != "" {
listMachineTypeReq = listMachineTypeReq.Filter(strings.TrimSpace(model.Filter.ValueString()))
@ -187,7 +198,7 @@ func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq
return
}
if err := mapDataSourceFields(ctx, sorted[0], &model); err != nil {
if err := mapDataSourceFields(ctx, sorted[0], &model, region); err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading machine type", fmt.Sprintf("Failed to translate API response: %v", err))
return
}
@ -199,7 +210,7 @@ func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq
tflog.Info(ctx, "Successfully read machine type")
}
func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, model *DataSourceModel) error {
func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, model *DataSourceModel, region string) error {
if machineType == nil || model == nil {
return fmt.Errorf("nil input provided")
}
@ -208,7 +219,8 @@ func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, mod
return fmt.Errorf("machine type name is missing")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), *machineType.Name)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, *machineType.Name)
model.Region = types.StringValue(region)
model.Name = types.StringPointerValue(machineType.Name)
model.Description = types.StringPointerValue(machineType.Description)
model.Disk = types.Int64PointerValue(machineType.Disk)

View file

@ -13,32 +13,39 @@ import (
)
func TestMapDataSourceFields(t *testing.T) {
type args struct {
initial DataSourceModel
input *iaas.MachineType
region string
}
tests := []struct {
name string
initial DataSourceModel
input *iaas.MachineType
args args
expected DataSourceModel
expectError bool
}{
{
name: "valid simple values",
initial: DataSourceModel{
ProjectId: types.StringValue("pid"),
},
input: &iaas.MachineType{
Name: utils.Ptr("s1.2"),
Description: utils.Ptr("general-purpose small"),
Disk: utils.Ptr(int64(20)),
Ram: utils.Ptr(int64(2048)),
Vcpus: utils.Ptr(int64(2)),
ExtraSpecs: &map[string]interface{}{
"cpu": "amd-epycrome-7702",
"overcommit": "1",
"environment": "general",
args: args{
initial: DataSourceModel{
ProjectId: types.StringValue("pid"),
},
input: &iaas.MachineType{
Name: utils.Ptr("s1.2"),
Description: utils.Ptr("general-purpose small"),
Disk: utils.Ptr(int64(20)),
Ram: utils.Ptr(int64(2048)),
Vcpus: utils.Ptr(int64(2)),
ExtraSpecs: &map[string]interface{}{
"cpu": "amd-epycrome-7702",
"overcommit": "1",
"environment": "general",
},
},
region: "eu01",
},
expected: DataSourceModel{
Id: types.StringValue("pid,s1.2"),
Id: types.StringValue("pid,eu01,s1.2"),
ProjectId: types.StringValue("pid"),
Name: types.StringValue("s1.2"),
Description: types.StringValue("general-purpose small"),
@ -50,42 +57,50 @@ func TestMapDataSourceFields(t *testing.T) {
"overcommit": types.StringValue("1"),
"environment": types.StringValue("general"),
}),
Region: types.StringValue("eu01"),
},
expectError: false,
},
{
name: "missing name should fail",
initial: DataSourceModel{
ProjectId: types.StringValue("pid-456"),
},
input: &iaas.MachineType{
Description: utils.Ptr("gp-medium"),
args: args{
initial: DataSourceModel{
ProjectId: types.StringValue("pid-456"),
},
input: &iaas.MachineType{
Description: utils.Ptr("gp-medium"),
},
},
expected: DataSourceModel{},
expectError: true,
},
{
name: "nil machineType should fail",
initial: DataSourceModel{},
input: nil,
name: "nil machineType should fail",
args: args{
initial: DataSourceModel{},
input: nil,
},
expected: DataSourceModel{},
expectError: true,
},
{
name: "empty extraSpecs should return null map",
initial: DataSourceModel{
ProjectId: types.StringValue("pid-789"),
},
input: &iaas.MachineType{
Name: utils.Ptr("m1.noextras"),
Description: utils.Ptr("no extras"),
Disk: utils.Ptr(int64(10)),
Ram: utils.Ptr(int64(1024)),
Vcpus: utils.Ptr(int64(1)),
ExtraSpecs: &map[string]interface{}{},
args: args{
initial: DataSourceModel{
ProjectId: types.StringValue("pid-789"),
},
input: &iaas.MachineType{
Name: utils.Ptr("m1.noextras"),
Description: utils.Ptr("no extras"),
Disk: utils.Ptr(int64(10)),
Ram: utils.Ptr(int64(1024)),
Vcpus: utils.Ptr(int64(1)),
ExtraSpecs: &map[string]interface{}{},
},
region: "eu01",
},
expected: DataSourceModel{
Id: types.StringValue("pid-789,m1.noextras"),
Id: types.StringValue("pid-789,eu01,m1.noextras"),
ProjectId: types.StringValue("pid-789"),
Name: types.StringValue("m1.noextras"),
Description: types.StringValue("no extras"),
@ -93,24 +108,28 @@ func TestMapDataSourceFields(t *testing.T) {
Ram: types.Int64Value(1024),
Vcpus: types.Int64Value(1),
ExtraSpecs: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
expectError: false,
},
{
name: "nil extrasSpecs should return null map",
initial: DataSourceModel{
ProjectId: types.StringValue("pid-987"),
},
input: &iaas.MachineType{
Name: utils.Ptr("g1.nil"),
Description: utils.Ptr("missing extras"),
Disk: utils.Ptr(int64(40)),
Ram: utils.Ptr(int64(8096)),
Vcpus: utils.Ptr(int64(4)),
ExtraSpecs: nil,
args: args{
initial: DataSourceModel{
ProjectId: types.StringValue("pid-987"),
},
input: &iaas.MachineType{
Name: utils.Ptr("g1.nil"),
Description: utils.Ptr("missing extras"),
Disk: utils.Ptr(int64(40)),
Ram: utils.Ptr(int64(8096)),
Vcpus: utils.Ptr(int64(4)),
ExtraSpecs: nil,
},
region: "eu01",
},
expected: DataSourceModel{
Id: types.StringValue("pid-987,g1.nil"),
Id: types.StringValue("pid-987,eu01,g1.nil"),
ProjectId: types.StringValue("pid-987"),
Name: types.StringValue("g1.nil"),
Description: types.StringValue("missing extras"),
@ -118,24 +137,27 @@ func TestMapDataSourceFields(t *testing.T) {
Ram: types.Int64Value(8096),
Vcpus: types.Int64Value(4),
ExtraSpecs: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
expectError: false,
},
{
name: "invalid extraSpecs with non-string values",
initial: DataSourceModel{
ProjectId: types.StringValue("test-err"),
},
input: &iaas.MachineType{
Name: utils.Ptr("invalid"),
Description: utils.Ptr("bad map"),
Disk: utils.Ptr(int64(10)),
Ram: utils.Ptr(int64(4096)),
Vcpus: utils.Ptr(int64(2)),
ExtraSpecs: &map[string]interface{}{
"cpu": "intel",
"burst": true, // not a string
"gen": 8, // not a string
args: args{
initial: DataSourceModel{
ProjectId: types.StringValue("test-err"),
},
input: &iaas.MachineType{
Name: utils.Ptr("invalid"),
Description: utils.Ptr("bad map"),
Disk: utils.Ptr(int64(10)),
Ram: utils.Ptr(int64(4096)),
Vcpus: utils.Ptr(int64(2)),
ExtraSpecs: &map[string]interface{}{
"cpu": "intel",
"burst": true, // not a string
"gen": 8, // not a string
},
},
},
expected: DataSourceModel{},
@ -145,7 +167,7 @@ func TestMapDataSourceFields(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := mapDataSourceFields(context.Background(), tt.input, &tt.initial)
err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.initial, tt.args.region)
if tt.expectError {
if err == nil {
t.Errorf("expected error but got none")
@ -157,13 +179,13 @@ func TestMapDataSourceFields(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}
diff := cmp.Diff(tt.expected, tt.initial)
diff := cmp.Diff(tt.expected, tt.args.initial)
if diff != "" {
t.Errorf("unexpected diff (-want +got):\n%s", diff)
}
// Extra sanity check for proper ID format
if id := tt.initial.Id.ValueString(); !strings.HasPrefix(id, tt.initial.ProjectId.ValueString()+",") {
if id := tt.args.initial.Id.ValueString(); !strings.HasPrefix(id, tt.args.initial.ProjectId.ValueString()+",") {
t.Errorf("unexpected ID format: got %q", id)
}
})

View file

@ -2,14 +2,11 @@ package network
import (
"context"
"fmt"
"net"
"net/http"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
@ -18,7 +15,9 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
@ -27,6 +26,30 @@ var (
_ datasource.DataSource = &networkDataSource{}
)
type DataSourceModel struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
NetworkId types.String `tfsdk:"network_id"`
Name types.String `tfsdk:"name"`
Nameservers types.List `tfsdk:"nameservers"`
IPv4Gateway types.String `tfsdk:"ipv4_gateway"`
IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"`
IPv4Prefix types.String `tfsdk:"ipv4_prefix"`
IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"`
Prefixes types.List `tfsdk:"prefixes"`
IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"`
IPv6Gateway types.String `tfsdk:"ipv6_gateway"`
IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"`
IPv6Prefix types.String `tfsdk:"ipv6_prefix"`
IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"`
IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"`
PublicIP types.String `tfsdk:"public_ip"`
Labels types.Map `tfsdk:"labels"`
Routed types.Bool `tfsdk:"routed"`
Region types.String `tfsdk:"region"`
RoutingTableID types.String `tfsdk:"routing_table_id"`
}
// NewNetworkDataSource is a helper function to simplify the provider implementation.
func NewNetworkDataSource() datasource.DataSource {
return &networkDataSource{}
@ -34,11 +57,8 @@ func NewNetworkDataSource() datasource.DataSource {
// networkDataSource is the data source implementation.
type networkDataSource struct {
client *iaas.APIClient
// alphaClient will be used in case the experimental flag "network" is set
alphaClient *iaasalpha.APIClient
isExperimental bool
providerData core.ProviderData
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -53,24 +73,11 @@ func (d *networkDataSource) Configure(ctx context.Context, req datasource.Config
return
}
d.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &d.providerData, features.NetworkExperiment, "stackit_network", core.Datasource, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
if d.isExperimental {
alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
d.alphaClient = alphaApiClient
} else {
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
d.client = apiClient
}
d.client = apiClient
tflog.Info(ctx, "IaaS client configured")
}
@ -197,9 +204,199 @@ func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
// Read refreshes the Terraform state with the latest data.
func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
if !d.isExperimental {
v1network.DatasourceRead(ctx, req, resp, d.client)
} else {
v2network.DatasourceRead(ctx, req, resp, d.alphaClient, d.providerData)
var model DataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
networkResp, err := d.client.GetNetwork(ctx, projectId, region, networkId).Execute()
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading network",
fmt.Sprintf("Network with ID %q does not exist in project %q.", networkId, projectId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
},
)
resp.State.RemoveResource(ctx)
return
}
err = mapDataSourceFields(ctx, networkResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network read")
}
func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model *DataSourceModel, region string) error {
if networkResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var networkId string
if model.NetworkId.ValueString() != "" {
networkId = model.NetworkId.ValueString()
} else if networkResp.Id != nil {
networkId = *networkResp.Id
} else {
return fmt.Errorf("network id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId)
labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels)
if err != nil {
return err
}
// IPv4
if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil {
model.Nameservers = types.ListNull(types.StringType)
model.IPv4Nameservers = types.ListNull(types.StringType)
} else {
respNameservers := *networkResp.Ipv4.Nameservers
modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers)
modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers)
if err != nil {
return fmt.Errorf("get current network nameservers from model: %w", err)
}
if errIpv4 != nil {
return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4)
}
reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers)
reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers)
nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers)
ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers)
if diags.HasError() {
return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags))
}
if ipv4Diags.HasError() {
return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags))
}
model.Nameservers = nameserversTF
model.IPv4Nameservers = ipv4NameserversTF
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil {
model.Prefixes = types.ListNull(types.StringType)
model.IPv4Prefixes = types.ListNull(types.StringType)
} else {
respPrefixes := *networkResp.Ipv4.Prefixes
prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes)
if diags.HasError() {
return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixes) > 0 {
model.IPv4Prefix = types.StringValue(respPrefixes[0])
_, netmask, err := net.ParseCIDR(respPrefixes[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv4PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv4PrefixLength = types.Int64Value(int64(ones))
}
}
model.Prefixes = prefixesTF
model.IPv4Prefixes = prefixesTF
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil {
model.IPv4Gateway = types.StringNull()
} else {
model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway())
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil {
model.PublicIP = types.StringNull()
} else {
model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp)
}
// IPv6
if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil {
model.IPv6Nameservers = types.ListNull(types.StringType)
} else {
respIPv6Nameservers := *networkResp.Ipv6.Nameservers
modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers)
if errIpv6 != nil {
return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6)
}
reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers)
ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers)
if ipv6Diags.HasError() {
return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags))
}
model.IPv6Nameservers = ipv6NameserversTF
}
if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil {
model.IPv6Prefixes = types.ListNull(types.StringType)
} else {
respPrefixesV6 := *networkResp.Ipv6.Prefixes
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
if diags.HasError() {
return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixesV6) > 0 {
model.IPv6Prefix = types.StringValue(respPrefixesV6[0])
_, netmask, err := net.ParseCIDR(respPrefixesV6[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv6PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv6PrefixLength = types.Int64Value(int64(ones))
}
}
model.IPv6Prefixes = prefixesV6TF
}
if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil {
model.IPv6Gateway = types.StringNull()
} else {
model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway())
}
model.RoutingTableID = types.StringNull()
if networkResp.RoutingTableId != nil {
model.RoutingTableID = types.StringValue(*networkResp.RoutingTableId)
}
model.NetworkId = types.StringValue(networkId)
model.Name = types.StringPointerValue(networkResp.Name)
model.Labels = labels
model.Routed = types.BoolPointerValue(networkResp.Routed)
model.Region = types.StringValue(region)
return nil
}

View file

@ -1,15 +1,15 @@
package v2network
package network
import (
"context"
"testing"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
)
const (
@ -19,26 +19,26 @@ const (
func TestMapDataSourceFields(t *testing.T) {
tests := []struct {
description string
state networkModel.DataSourceModel
input *iaasalpha.Network
state DataSourceModel
input *iaas.Network
region string
expected networkModel.DataSourceModel
expected DataSourceModel
isValid bool
}{
{
"id_ok",
networkModel.DataSourceModel{
DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv4: &iaasalpha.NetworkIPv4{
Gateway: iaasalpha.NewNullableString(nil),
Ipv4: &iaas.NetworkIPv4{
Gateway: iaas.NewNullableString(nil),
},
},
testRegion,
networkModel.DataSourceModel{
DataSourceModel{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -64,14 +64,14 @@ func TestMapDataSourceFields(t *testing.T) {
},
{
"values_ok",
networkModel.DataSourceModel{
DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Name: utils.Ptr("name"),
Ipv4: &iaasalpha.NetworkIPv4{
Ipv4: &iaas.NetworkIPv4{
Nameservers: &[]string{
"ns1",
"ns2",
@ -81,9 +81,9 @@ func TestMapDataSourceFields(t *testing.T) {
"10.100.10.0/16",
},
PublicIp: utils.Ptr("publicIp"),
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
Ipv6: &iaasalpha.NetworkIPv6{
Ipv6: &iaas.NetworkIPv6{
Nameservers: &[]string{
"ns1",
"ns2",
@ -92,7 +92,7 @@ func TestMapDataSourceFields(t *testing.T) {
"fd12:3456:789a:1::/64",
"fd12:3456:789a:2::/64",
},
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
Labels: &map[string]interface{}{
"key": "value",
@ -100,7 +100,7 @@ func TestMapDataSourceFields(t *testing.T) {
Routed: utils.Ptr(true),
},
testRegion,
networkModel.DataSourceModel{
DataSourceModel{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -146,7 +146,7 @@ func TestMapDataSourceFields(t *testing.T) {
},
{
"ipv4_nameservers_changed_outside_tf",
networkModel.DataSourceModel{
DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
@ -158,9 +158,9 @@ func TestMapDataSourceFields(t *testing.T) {
types.StringValue("ns2"),
}),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv4: &iaasalpha.NetworkIPv4{
Ipv4: &iaas.NetworkIPv4{
Nameservers: &[]string{
"ns2",
"ns3",
@ -168,7 +168,7 @@ func TestMapDataSourceFields(t *testing.T) {
},
},
testRegion,
networkModel.DataSourceModel{
DataSourceModel{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -192,7 +192,7 @@ func TestMapDataSourceFields(t *testing.T) {
},
{
"ipv6_nameservers_changed_outside_tf",
networkModel.DataSourceModel{
DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
@ -200,9 +200,9 @@ func TestMapDataSourceFields(t *testing.T) {
types.StringValue("ns2"),
}),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv6: &iaasalpha.NetworkIPv6{
Ipv6: &iaas.NetworkIPv6{
Nameservers: &[]string{
"ns2",
"ns3",
@ -210,7 +210,7 @@ func TestMapDataSourceFields(t *testing.T) {
},
},
testRegion,
networkModel.DataSourceModel{
DataSourceModel{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -231,7 +231,7 @@ func TestMapDataSourceFields(t *testing.T) {
},
{
"ipv4_prefixes_changed_outside_tf",
networkModel.DataSourceModel{
DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Prefixes: types.ListValueMust(types.StringType, []attr.Value{
@ -239,9 +239,9 @@ func TestMapDataSourceFields(t *testing.T) {
types.StringValue("10.100.10.0/16"),
}),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv4: &iaasalpha.NetworkIPv4{
Ipv4: &iaas.NetworkIPv4{
Prefixes: &[]string{
"10.100.20.0/16",
"10.100.10.0/16",
@ -249,7 +249,7 @@ func TestMapDataSourceFields(t *testing.T) {
},
},
testRegion,
networkModel.DataSourceModel{
DataSourceModel{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -276,7 +276,7 @@ func TestMapDataSourceFields(t *testing.T) {
},
{
"ipv6_prefixes_changed_outside_tf",
networkModel.DataSourceModel{
DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{
@ -284,9 +284,9 @@ func TestMapDataSourceFields(t *testing.T) {
types.StringValue("fd12:3456:789a:2::/64"),
}),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv6: &iaasalpha.NetworkIPv6{
Ipv6: &iaas.NetworkIPv6{
Prefixes: &[]string{
"fd12:3456:789a:3::/64",
"fd12:3456:789a:4::/64",
@ -294,7 +294,7 @@ func TestMapDataSourceFields(t *testing.T) {
},
},
testRegion,
networkModel.DataSourceModel{
DataSourceModel{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -318,15 +318,15 @@ func TestMapDataSourceFields(t *testing.T) {
},
{
"ipv4_ipv6_gateway_nil",
networkModel.DataSourceModel{
DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
},
testRegion,
networkModel.DataSourceModel{
DataSourceModel{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -350,20 +350,20 @@ func TestMapDataSourceFields(t *testing.T) {
},
{
"response_nil_fail",
networkModel.DataSourceModel{},
DataSourceModel{},
nil,
testRegion,
networkModel.DataSourceModel{},
DataSourceModel{},
false,
},
{
"no_resource_id",
networkModel.DataSourceModel{
DataSourceModel{
ProjectId: types.StringValue("pid"),
},
&iaasalpha.Network{},
&iaas.Network{},
testRegion,
networkModel.DataSourceModel{},
DataSourceModel{},
false,
},
}

View file

@ -3,9 +3,13 @@ package network
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
@ -18,16 +22,12 @@ import (
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
@ -37,6 +37,7 @@ var (
_ resource.Resource = &networkResource{}
_ resource.ResourceWithConfigure = &networkResource{}
_ resource.ResourceWithImportState = &networkResource{}
_ resource.ResourceWithModifyPlan = &networkResource{}
)
const (
@ -46,6 +47,32 @@ const (
"In cases where `ipv4_nameservers` are defined within the resource, the existing behavior will remain unchanged."
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
NetworkId types.String `tfsdk:"network_id"`
Name types.String `tfsdk:"name"`
Nameservers types.List `tfsdk:"nameservers"`
IPv4Gateway types.String `tfsdk:"ipv4_gateway"`
IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"`
IPv4Prefix types.String `tfsdk:"ipv4_prefix"`
IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"`
Prefixes types.List `tfsdk:"prefixes"`
IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"`
IPv6Gateway types.String `tfsdk:"ipv6_gateway"`
IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"`
IPv6Prefix types.String `tfsdk:"ipv6_prefix"`
IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"`
IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"`
PublicIP types.String `tfsdk:"public_ip"`
Labels types.Map `tfsdk:"labels"`
Routed types.Bool `tfsdk:"routed"`
NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"`
NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"`
Region types.String `tfsdk:"region"`
RoutingTableID types.String `tfsdk:"routing_table_id"`
}
// NewNetworkResource is a helper function to simplify the provider implementation.
func NewNetworkResource() resource.Resource {
return &networkResource{}
@ -53,11 +80,8 @@ func NewNetworkResource() resource.Resource {
// networkResource is the resource implementation.
type networkResource struct {
client *iaas.APIClient
// alphaClient will be used in case the experimental flag "network" is set
alphaClient *iaasalpha.APIClient
isExperimental bool
providerData core.ProviderData
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -73,31 +97,18 @@ func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureR
return
}
r.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &r.providerData, features.NetworkExperiment, "stackit_network", core.Resource, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
if r.isExperimental {
alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
r.alphaClient = alphaApiClient
} else {
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
r.client = apiClient
}
r.client = apiClient
tflog.Info(ctx, "IaaS client configured")
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel model.Model
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
@ -107,7 +118,7 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla
return
}
var planModel model.Model
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
@ -118,10 +129,6 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla
addIPv4Warning(&resp.Diagnostics)
}
// If the v1 api is used, it's not required to get the fallback region because it isn't used
if !r.isExperimental {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
@ -134,7 +141,7 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla
}
func (r *networkResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var resourceModel model.Model
var resourceModel Model
resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...)
if resp.Diagnostics.HasError() {
return
@ -143,14 +150,6 @@ func (r *networkResource) ValidateConfig(ctx context.Context, req resource.Valid
if !resourceModel.Nameservers.IsUnknown() && !resourceModel.IPv4Nameservers.IsUnknown() && !resourceModel.Nameservers.IsNull() && !resourceModel.IPv4Nameservers.IsNull() {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "You cannot provide both the `nameservers` and `ipv4_nameservers` fields simultaneously. Please remove the deprecated `nameservers` field, and use `ipv4_nameservers` to configure nameservers for IPv4.")
}
if !r.isExperimental {
if !utils.IsUndefined(resourceModel.Region) {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the `region` is not supported yet. This can only be configured when the experiments `network` is set.")
}
if !utils.IsUndefined(resourceModel.RoutingTableID) {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the field `routing_table_id` is not supported yet. This can only be configured when the experiments `network` is set.")
}
}
}
// ConfigValidators validates the resource configuration
@ -192,7 +191,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`network_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -359,7 +358,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
},
"routing_table_id": schema.StringAttribute{
Description: "Can only be used when experimental \"network\" is set.\nThe ID of the routing table associated with the network.",
Description: "The ID of the routing table associated with the network.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
@ -374,7 +373,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
Description: "Can only be used when experimental \"network\" is set.\nThe resource region. If not defined, the provider region is used.",
Description: "The resource region. If not defined, the provider region is used.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplaceIfConfigured(),
},
@ -386,59 +385,568 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
// Create creates the resource and sets the initial Terraform state.
func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var planModel model.Model
diags := req.Plan.Get(ctx, &planModel)
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// When IPv4Nameserver is not set, print warning that the behavior of ipv4_nameservers will change
if utils.IsUndefined(planModel.IPv4Nameservers) {
if utils.IsUndefined(model.IPv4Nameservers) {
addIPv4Warning(&resp.Diagnostics)
}
if !r.isExperimental {
v1network.Create(ctx, req, resp, r.client)
} else {
v2network.Create(ctx, req, resp, r.alphaClient)
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create new network
network, err := r.client.CreateNetwork(ctx, projectId, region).CreateNetworkPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err))
return
}
networkId := *network.Id
ctx = tflog.SetField(ctx, "network_id", networkId)
network, err = wait.CreateNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err))
return
}
// Map response body to schema
err = mapFields(ctx, network, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network created")
}
// Read refreshes the Terraform state with the latest data.
func (r *networkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
if !r.isExperimental {
v1network.Read(ctx, req, resp, r.client)
} else {
v2network.Read(ctx, req, resp, r.alphaClient, r.providerData)
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "region", region)
networkResp, err := r.client.GetNetwork(ctx, projectId, region, networkId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err))
return
}
// Map response body to schema
err = mapFields(ctx, networkResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network read")
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
if !r.isExperimental {
v1network.Update(ctx, req, resp, r.client)
} else {
v2network.Update(ctx, req, resp, r.alphaClient)
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "region", region)
// Retrieve values from state
var stateModel Model
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Generate API request body from model
payload, err := toUpdatePayload(ctx, &model, &stateModel)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Update existing network
err = r.client.PartialUpdateNetwork(ctx, projectId, region, networkId).PartialUpdateNetworkPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err))
return
}
waitResp, err := wait.UpdateNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err))
return
}
err = mapFields(ctx, waitResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network updated")
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *networkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
if !r.isExperimental {
v1network.Delete(ctx, req, resp, r.client)
} else {
v2network.Delete(ctx, req, resp, r.alphaClient)
// Retrieve values from state
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := model.Region.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "region", region)
// Delete existing network
err := r.client.DeleteNetwork(ctx, projectId, region, networkId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err))
return
}
_, err = wait.DeleteNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err))
return
}
tflog.Info(ctx, "Network deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,network_id
// The expected format of the resource import identifier is: project_id,region,network_id
func (r *networkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
if !r.isExperimental {
v1network.ImportState(ctx, req, resp)
} else {
v2network.ImportState(ctx, req, resp)
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing network",
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
region := idParts[1]
networkId := idParts[2]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_id", networkId)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...)
tflog.Info(ctx, "Network state imported")
}
func mapFields(ctx context.Context, networkResp *iaas.Network, model *Model, region string) error {
if networkResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var networkId string
if model.NetworkId.ValueString() != "" {
networkId = model.NetworkId.ValueString()
} else if networkResp.Id != nil {
networkId = *networkResp.Id
} else {
return fmt.Errorf("network id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId)
labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels)
if err != nil {
return err
}
// IPv4
if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil {
model.Nameservers = types.ListNull(types.StringType)
model.IPv4Nameservers = types.ListNull(types.StringType)
} else {
respNameservers := *networkResp.Ipv4.Nameservers
modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers)
modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers)
if err != nil {
return fmt.Errorf("get current network nameservers from model: %w", err)
}
if errIpv4 != nil {
return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4)
}
reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers)
reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers)
nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers)
ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers)
if diags.HasError() {
return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags))
}
if ipv4Diags.HasError() {
return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags))
}
model.Nameservers = nameserversTF
model.IPv4Nameservers = ipv4NameserversTF
}
model.IPv4PrefixLength = types.Int64Null()
if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil {
model.Prefixes = types.ListNull(types.StringType)
model.IPv4Prefixes = types.ListNull(types.StringType)
} else {
respPrefixes := *networkResp.Ipv4.Prefixes
prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes)
if diags.HasError() {
return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixes) > 0 {
model.IPv4Prefix = types.StringValue(respPrefixes[0])
_, netmask, err := net.ParseCIDR(respPrefixes[0])
if err != nil {
tflog.Error(ctx, fmt.Sprintf("ipv4_prefix_length: %+v", err))
// silently ignore parsing error for the netmask
model.IPv4PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv4PrefixLength = types.Int64Value(int64(ones))
}
}
model.Prefixes = prefixesTF
model.IPv4Prefixes = prefixesTF
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil {
model.IPv4Gateway = types.StringNull()
} else {
model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway())
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil {
model.PublicIP = types.StringNull()
} else {
model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp)
}
// IPv6
if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil {
model.IPv6Nameservers = types.ListNull(types.StringType)
} else {
respIPv6Nameservers := *networkResp.Ipv6.Nameservers
modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers)
if errIpv6 != nil {
return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6)
}
reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers)
ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers)
if ipv6Diags.HasError() {
return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags))
}
model.IPv6Nameservers = ipv6NameserversTF
}
model.IPv6PrefixLength = types.Int64Null()
model.IPv6Prefix = types.StringNull()
if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil {
model.IPv6Prefixes = types.ListNull(types.StringType)
} else {
respPrefixesV6 := *networkResp.Ipv6.Prefixes
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
if diags.HasError() {
return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixesV6) > 0 {
model.IPv6Prefix = types.StringValue(respPrefixesV6[0])
_, netmask, err := net.ParseCIDR(respPrefixesV6[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv6PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv6PrefixLength = types.Int64Value(int64(ones))
}
}
model.IPv6Prefixes = prefixesV6TF
}
if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil {
model.IPv6Gateway = types.StringNull()
} else {
model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway())
}
model.RoutingTableID = types.StringPointerValue(networkResp.RoutingTableId)
model.NetworkId = types.StringValue(networkId)
model.Name = types.StringPointerValue(networkResp.Name)
model.Labels = labels
model.Routed = types.BoolPointerValue(networkResp.Routed)
model.Region = types.StringValue(region)
return nil
}
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
var modelIPv6Nameservers []string
// Is true when IPv6Nameservers is not null or unset
if !utils.IsUndefined(model.IPv6Nameservers) {
// If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice.
// empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set
modelIPv6Nameservers = []string{}
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
ipv6NameserverString, ok := ipv6ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
}
}
var ipv6Body *iaas.CreateNetworkIPv6
if !utils.IsUndefined(model.IPv6PrefixLength) {
ipv6Body = &iaas.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefixLength: &iaas.CreateNetworkIPv6WithPrefixLength{
PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength),
},
}
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
if modelIPv6Nameservers != nil {
ipv6Body.CreateNetworkIPv6WithPrefixLength.Nameservers = &modelIPv6Nameservers
}
} else if !utils.IsUndefined(model.IPv6Prefix) {
var gateway *iaas.NullableString
if model.NoIPv6Gateway.ValueBool() {
gateway = iaas.NewNullableString(nil)
} else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) {
gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
}
ipv6Body = &iaas.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{
Gateway: gateway,
Prefix: conversion.StringValueToPointer(model.IPv6Prefix),
},
}
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
if modelIPv6Nameservers != nil {
ipv6Body.CreateNetworkIPv6WithPrefix.Nameservers = &modelIPv6Nameservers
}
}
modelIPv4Nameservers := []string{}
var modelIPv4List []attr.Value
if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) {
modelIPv4List = model.IPv4Nameservers.Elements()
} else {
modelIPv4List = model.Nameservers.Elements()
}
for _, ipv4ns := range modelIPv4List {
ipv4NameserverString, ok := ipv4ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString())
}
var ipv4Body *iaas.CreateNetworkIPv4
if !utils.IsUndefined(model.IPv4PrefixLength) {
ipv4Body = &iaas.CreateNetworkIPv4{
CreateNetworkIPv4WithPrefixLength: &iaas.CreateNetworkIPv4WithPrefixLength{
Nameservers: &modelIPv4Nameservers,
PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength),
},
}
} else if !utils.IsUndefined(model.IPv4Prefix) {
var gateway *iaas.NullableString
if model.NoIPv4Gateway.ValueBool() {
gateway = iaas.NewNullableString(nil)
} else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) {
gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway))
}
ipv4Body = &iaas.CreateNetworkIPv4{
CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{
Nameservers: &modelIPv4Nameservers,
Prefix: conversion.StringValueToPointer(model.IPv4Prefix),
Gateway: gateway,
},
}
}
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
payload := iaas.CreateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
Routed: conversion.BoolValueToPointer(model.Routed),
Ipv4: ipv4Body,
Ipv6: ipv6Body,
RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID),
}
return &payload, nil
}
func toUpdatePayload(ctx context.Context, model, stateModel *Model) (*iaas.PartialUpdateNetworkPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
var modelIPv6Nameservers []string
// Is true when IPv6Nameservers is not null or unset
if !utils.IsUndefined(model.IPv6Nameservers) {
// If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice.
// empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set
modelIPv6Nameservers = []string{}
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
ipv6NameserverString, ok := ipv6ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
}
}
var ipv6Body *iaas.UpdateNetworkIPv6Body
if modelIPv6Nameservers != nil || !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) {
ipv6Body = &iaas.UpdateNetworkIPv6Body{}
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
if modelIPv6Nameservers != nil {
ipv6Body.Nameservers = &modelIPv6Nameservers
}
if model.NoIPv6Gateway.ValueBool() {
ipv6Body.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) {
ipv6Body.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
}
}
modelIPv4Nameservers := []string{}
var modelIPv4List []attr.Value
if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) {
modelIPv4List = model.IPv4Nameservers.Elements()
} else {
modelIPv4List = model.Nameservers.Elements()
}
for _, ipv4ns := range modelIPv4List {
ipv4NameserverString, ok := ipv4ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString())
}
var ipv4Body *iaas.UpdateNetworkIPv4Body
if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() {
ipv4Body = &iaas.UpdateNetworkIPv4Body{
Nameservers: &modelIPv4Nameservers,
}
if model.NoIPv4Gateway.ValueBool() {
ipv4Body.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) {
ipv4Body.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway))
}
}
currentLabels := stateModel.Labels
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
payload := iaas.PartialUpdateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
Ipv4: ipv4Body,
Ipv6: ipv6Body,
RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID),
}
return &payload, nil
}
func addIPv4Warning(diags *diag.Diagnostics) {

View file

@ -1,4 +1,4 @@
package v2network
package network
import (
"context"
@ -8,34 +8,33 @@ import (
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
func TestMapFields(t *testing.T) {
const testRegion = "region"
tests := []struct {
description string
state model.Model
input *iaasalpha.Network
state Model
input *iaas.Network
region string
expected model.Model
expected Model
isValid bool
}{
{
"id_ok",
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv4: &iaasalpha.NetworkIPv4{
Gateway: iaasalpha.NewNullableString(nil),
Ipv4: &iaas.NetworkIPv4{
Gateway: iaas.NewNullableString(nil),
},
},
testRegion,
model.Model{
Model{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -61,14 +60,14 @@ func TestMapFields(t *testing.T) {
},
{
"values_ok",
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Name: utils.Ptr("name"),
Ipv4: &iaasalpha.NetworkIPv4{
Ipv4: &iaas.NetworkIPv4{
Nameservers: utils.Ptr([]string{"ns1", "ns2"}),
Prefixes: utils.Ptr(
[]string{
@ -77,15 +76,15 @@ func TestMapFields(t *testing.T) {
},
),
PublicIp: utils.Ptr("publicIp"),
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
Ipv6: &iaasalpha.NetworkIPv6{
Ipv6: &iaas.NetworkIPv6{
Nameservers: utils.Ptr([]string{"ns1", "ns2"}),
Prefixes: utils.Ptr([]string{
"fd12:3456:789a:1::/64",
"fd12:3456:789b:1::/64",
}),
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
Labels: &map[string]interface{}{
"key": "value",
@ -93,7 +92,7 @@ func TestMapFields(t *testing.T) {
Routed: utils.Ptr(true),
},
testRegion,
model.Model{
Model{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -139,7 +138,7 @@ func TestMapFields(t *testing.T) {
},
{
"ipv4_nameservers_changed_outside_tf",
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
@ -151,9 +150,9 @@ func TestMapFields(t *testing.T) {
types.StringValue("ns2"),
}),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv4: &iaasalpha.NetworkIPv4{
Ipv4: &iaas.NetworkIPv4{
Nameservers: utils.Ptr([]string{
"ns2",
"ns3",
@ -161,7 +160,7 @@ func TestMapFields(t *testing.T) {
},
},
testRegion,
model.Model{
Model{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -185,7 +184,7 @@ func TestMapFields(t *testing.T) {
},
{
"ipv6_nameservers_changed_outside_tf",
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
@ -193,9 +192,9 @@ func TestMapFields(t *testing.T) {
types.StringValue("ns2"),
}),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv6: &iaasalpha.NetworkIPv6{
Ipv6: &iaas.NetworkIPv6{
Nameservers: utils.Ptr([]string{
"ns2",
"ns3",
@ -203,7 +202,7 @@ func TestMapFields(t *testing.T) {
},
},
testRegion,
model.Model{
Model{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -224,7 +223,7 @@ func TestMapFields(t *testing.T) {
},
{
"ipv4_prefixes_changed_outside_tf",
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Prefixes: types.ListValueMust(types.StringType, []attr.Value{
@ -232,9 +231,9 @@ func TestMapFields(t *testing.T) {
types.StringValue("10.100.10.0/24"),
}),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv4: &iaasalpha.NetworkIPv4{
Ipv4: &iaas.NetworkIPv4{
Prefixes: utils.Ptr(
[]string{
"192.168.54.0/24",
@ -244,7 +243,7 @@ func TestMapFields(t *testing.T) {
},
},
testRegion,
model.Model{
Model{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -271,7 +270,7 @@ func TestMapFields(t *testing.T) {
},
{
"ipv6_prefixes_changed_outside_tf",
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{
@ -279,9 +278,9 @@ func TestMapFields(t *testing.T) {
types.StringValue("fd12:3456:789a:2::/64"),
}),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
Ipv6: &iaasalpha.NetworkIPv6{
Ipv6: &iaas.NetworkIPv6{
Prefixes: utils.Ptr(
[]string{
"fd12:3456:789a:1::/64",
@ -291,7 +290,7 @@ func TestMapFields(t *testing.T) {
},
},
testRegion,
model.Model{
Model{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -315,15 +314,15 @@ func TestMapFields(t *testing.T) {
},
{
"ipv4_ipv6_gateway_nil",
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaasalpha.Network{
&iaas.Network{
Id: utils.Ptr("nid"),
},
testRegion,
model.Model{
Model{
Id: types.StringValue("pid,region,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
@ -347,20 +346,20 @@ func TestMapFields(t *testing.T) {
},
{
"response_nil_fail",
model.Model{},
Model{},
nil,
testRegion,
model.Model{},
Model{},
false,
},
{
"no_resource_id",
model.Model{
Model{
ProjectId: types.StringValue("pid"),
},
&iaasalpha.Network{},
&iaas.Network{},
testRegion,
model.Model{},
Model{},
false,
},
}
@ -386,13 +385,13 @@ func TestMapFields(t *testing.T) {
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *model.Model
expected *iaasalpha.CreateNetworkPayload
input *Model
expected *iaas.CreateNetworkPayload
isValid bool
}{
{
"default_ok",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
@ -405,11 +404,11 @@ func TestToCreatePayload(t *testing.T) {
IPv4Gateway: types.StringValue("gateway"),
IPv4Prefix: types.StringValue("prefix"),
},
&iaasalpha.CreateNetworkPayload{
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
Ipv4: &iaasalpha.CreateNetworkIPv4{
CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Ipv4: &iaas.CreateNetworkIPv4{
CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Nameservers: utils.Ptr([]string{
"ns1",
"ns2",
@ -426,7 +425,7 @@ func TestToCreatePayload(t *testing.T) {
},
{
"ipv4_nameservers_okay",
&model.Model{
&Model{
Name: types.StringValue("name"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
@ -439,11 +438,11 @@ func TestToCreatePayload(t *testing.T) {
IPv4Gateway: types.StringValue("gateway"),
IPv4Prefix: types.StringValue("prefix"),
},
&iaasalpha.CreateNetworkPayload{
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
Ipv4: &iaasalpha.CreateNetworkIPv4{
CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Ipv4: &iaas.CreateNetworkIPv4{
CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Nameservers: utils.Ptr([]string{
"ns1",
"ns2",
@ -460,7 +459,7 @@ func TestToCreatePayload(t *testing.T) {
},
{
"ipv6_default_ok",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
@ -473,11 +472,11 @@ func TestToCreatePayload(t *testing.T) {
IPv6Gateway: types.StringValue("gateway"),
IPv6Prefix: types.StringValue("prefix"),
},
&iaasalpha.CreateNetworkPayload{
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
Ipv6: &iaasalpha.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Ipv6: &iaas.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Nameservers: utils.Ptr([]string{
"ns1",
"ns2",
@ -494,7 +493,7 @@ func TestToCreatePayload(t *testing.T) {
},
{
"ipv6_nameserver_null",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListNull(types.StringType),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
@ -504,12 +503,12 @@ func TestToCreatePayload(t *testing.T) {
IPv6Gateway: types.StringValue("gateway"),
IPv6Prefix: types.StringValue("prefix"),
},
&iaasalpha.CreateNetworkPayload{
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
Ipv6: &iaasalpha.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{
Ipv6: &iaas.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{
Nameservers: nil,
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Prefix: utils.Ptr("prefix"),
},
},
@ -522,7 +521,7 @@ func TestToCreatePayload(t *testing.T) {
},
{
"ipv6_nameserver_empty_list",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
@ -532,12 +531,12 @@ func TestToCreatePayload(t *testing.T) {
IPv6Gateway: types.StringValue("gateway"),
IPv6Prefix: types.StringValue("prefix"),
},
&iaasalpha.CreateNetworkPayload{
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
Ipv6: &iaasalpha.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{
Ipv6: &iaas.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{
Nameservers: utils.Ptr([]string{}),
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Prefix: utils.Ptr("prefix"),
},
},
@ -559,7 +558,7 @@ func TestToCreatePayload(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{}))
diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{}))
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
@ -571,14 +570,14 @@ func TestToCreatePayload(t *testing.T) {
func TestToUpdatePayload(t *testing.T) {
tests := []struct {
description string
input *model.Model
state model.Model
expected *iaasalpha.PartialUpdateNetworkPayload
input *Model
state Model
expected *iaas.PartialUpdateNetworkPayload
isValid bool
}{
{
"default_ok",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
@ -590,15 +589,15 @@ func TestToUpdatePayload(t *testing.T) {
Routed: types.BoolValue(true),
IPv4Gateway: types.StringValue("gateway"),
},
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaasalpha.PartialUpdateNetworkPayload{
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
Ipv4: &iaasalpha.UpdateNetworkIPv4Body{
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Ipv4: &iaas.UpdateNetworkIPv4Body{
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Nameservers: utils.Ptr([]string{
"ns1",
"ns2",
@ -612,7 +611,7 @@ func TestToUpdatePayload(t *testing.T) {
},
{
"ipv4_nameservers_okay",
&model.Model{
&Model{
Name: types.StringValue("name"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
@ -624,15 +623,15 @@ func TestToUpdatePayload(t *testing.T) {
Routed: types.BoolValue(true),
IPv4Gateway: types.StringValue("gateway"),
},
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaasalpha.PartialUpdateNetworkPayload{
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
Ipv4: &iaasalpha.UpdateNetworkIPv4Body{
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Ipv4: &iaas.UpdateNetworkIPv4Body{
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Nameservers: utils.Ptr([]string{
"ns1",
"ns2",
@ -646,7 +645,7 @@ func TestToUpdatePayload(t *testing.T) {
},
{
"ipv4_gateway_nil",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
@ -657,14 +656,14 @@ func TestToUpdatePayload(t *testing.T) {
}),
Routed: types.BoolValue(true),
},
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaasalpha.PartialUpdateNetworkPayload{
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
Ipv4: &iaasalpha.UpdateNetworkIPv4Body{
Ipv4: &iaas.UpdateNetworkIPv4Body{
Nameservers: utils.Ptr([]string{
"ns1",
"ns2",
@ -678,7 +677,7 @@ func TestToUpdatePayload(t *testing.T) {
},
{
"ipv6_default_ok",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
@ -690,15 +689,15 @@ func TestToUpdatePayload(t *testing.T) {
Routed: types.BoolValue(true),
IPv6Gateway: types.StringValue("gateway"),
},
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaasalpha.PartialUpdateNetworkPayload{
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
Ipv6: &iaasalpha.UpdateNetworkIPv6Body{
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Ipv6: &iaas.UpdateNetworkIPv6Body{
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Nameservers: utils.Ptr([]string{
"ns1",
"ns2",
@ -712,7 +711,7 @@ func TestToUpdatePayload(t *testing.T) {
},
{
"ipv6_gateway_nil",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
@ -723,14 +722,14 @@ func TestToUpdatePayload(t *testing.T) {
}),
Routed: types.BoolValue(true),
},
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaasalpha.PartialUpdateNetworkPayload{
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
Ipv6: &iaasalpha.UpdateNetworkIPv6Body{
Ipv6: &iaas.UpdateNetworkIPv6Body{
Nameservers: utils.Ptr([]string{
"ns1",
"ns2",
@ -744,7 +743,7 @@ func TestToUpdatePayload(t *testing.T) {
},
{
"ipv6_nameserver_null",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListNull(types.StringType),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
@ -753,16 +752,16 @@ func TestToUpdatePayload(t *testing.T) {
Routed: types.BoolValue(true),
IPv6Gateway: types.StringValue("gateway"),
},
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaasalpha.PartialUpdateNetworkPayload{
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
Ipv6: &iaasalpha.UpdateNetworkIPv6Body{
Ipv6: &iaas.UpdateNetworkIPv6Body{
Nameservers: nil,
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
Labels: &map[string]interface{}{
"key": "value",
@ -772,7 +771,7 @@ func TestToUpdatePayload(t *testing.T) {
},
{
"ipv6_nameserver_empty_list",
&model.Model{
&Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
@ -781,16 +780,16 @@ func TestToUpdatePayload(t *testing.T) {
Routed: types.BoolValue(true),
IPv6Gateway: types.StringValue("gateway"),
},
model.Model{
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaasalpha.PartialUpdateNetworkPayload{
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
Ipv6: &iaasalpha.UpdateNetworkIPv6Body{
Ipv6: &iaas.UpdateNetworkIPv6Body{
Nameservers: utils.Ptr([]string{}),
Gateway: iaasalpha.NewNullableString(utils.Ptr("gateway")),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
Labels: &map[string]interface{}{
"key": "value",
@ -809,7 +808,7 @@ func TestToUpdatePayload(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{}))
diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{}))
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -1,53 +0,0 @@
package model
import "github.com/hashicorp/terraform-plugin-framework/types"
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
NetworkId types.String `tfsdk:"network_id"`
Name types.String `tfsdk:"name"`
Nameservers types.List `tfsdk:"nameservers"`
IPv4Gateway types.String `tfsdk:"ipv4_gateway"`
IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"`
IPv4Prefix types.String `tfsdk:"ipv4_prefix"`
IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"`
Prefixes types.List `tfsdk:"prefixes"`
IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"`
IPv6Gateway types.String `tfsdk:"ipv6_gateway"`
IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"`
IPv6Prefix types.String `tfsdk:"ipv6_prefix"`
IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"`
IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"`
PublicIP types.String `tfsdk:"public_ip"`
Labels types.Map `tfsdk:"labels"`
Routed types.Bool `tfsdk:"routed"`
NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"`
NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"`
Region types.String `tfsdk:"region"`
RoutingTableID types.String `tfsdk:"routing_table_id"`
}
type DataSourceModel struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
NetworkId types.String `tfsdk:"network_id"`
Name types.String `tfsdk:"name"`
Nameservers types.List `tfsdk:"nameservers"`
IPv4Gateway types.String `tfsdk:"ipv4_gateway"`
IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"`
IPv4Prefix types.String `tfsdk:"ipv4_prefix"`
IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"`
Prefixes types.List `tfsdk:"prefixes"`
IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"`
IPv6Gateway types.String `tfsdk:"ipv6_gateway"`
IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"`
IPv6Prefix types.String `tfsdk:"ipv6_prefix"`
IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"`
IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"`
PublicIP types.String `tfsdk:"public_ip"`
Labels types.Map `tfsdk:"labels"`
Routed types.Bool `tfsdk:"routed"`
Region types.String `tfsdk:"region"`
RoutingTableID types.String `tfsdk:"routing_table_id"`
}

View file

@ -1,208 +0,0 @@
package v1network
import (
"context"
"fmt"
"net"
"net/http"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)
func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform
var model networkModel.DataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
networkResp, err := client.GetNetwork(ctx, projectId, networkId).Execute()
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading network",
fmt.Sprintf("Network with ID %q does not exist in project %q.", networkId, projectId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
},
)
resp.State.RemoveResource(ctx)
return
}
ctx = core.LogResponse(ctx)
err = mapDataSourceFields(ctx, networkResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network read")
}
func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model *networkModel.DataSourceModel) error {
if networkResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var networkId string
if model.NetworkId.ValueString() != "" {
networkId = model.NetworkId.ValueString()
} else if networkResp.NetworkId != nil {
networkId = *networkResp.NetworkId
} else {
return fmt.Errorf("network id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), networkId)
labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels)
if err != nil {
return err
}
// IPv4
if networkResp.Nameservers == nil {
model.Nameservers = types.ListNull(types.StringType)
model.IPv4Nameservers = types.ListNull(types.StringType)
} else {
respNameservers := *networkResp.Nameservers
modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers)
modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers)
if err != nil {
return fmt.Errorf("get current network nameservers from model: %w", err)
}
if errIpv4 != nil {
return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4)
}
reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers)
reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers)
nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers)
ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers)
if diags.HasError() {
return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags))
}
if ipv4Diags.HasError() {
return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags))
}
model.Nameservers = nameserversTF
model.IPv4Nameservers = ipv4NameserversTF
}
if networkResp.Prefixes == nil {
model.Prefixes = types.ListNull(types.StringType)
model.IPv4Prefixes = types.ListNull(types.StringType)
} else {
respPrefixes := *networkResp.Prefixes
prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes)
if diags.HasError() {
return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixes) > 0 {
model.IPv4Prefix = types.StringValue(respPrefixes[0])
_, netmask, err := net.ParseCIDR(respPrefixes[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv4PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv4PrefixLength = types.Int64Value(int64(ones))
}
}
model.Prefixes = prefixesTF
model.IPv4Prefixes = prefixesTF
}
model.IPv4Gateway = types.StringNull()
if networkResp.Gateway != nil {
model.IPv4Gateway = types.StringPointerValue(networkResp.GetGateway())
}
// IPv6
if networkResp.NameserversV6 == nil {
model.IPv6Nameservers = types.ListNull(types.StringType)
} else {
respIPv6Nameservers := *networkResp.NameserversV6
modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers)
if errIpv6 != nil {
return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6)
}
reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers)
ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers)
if ipv6Diags.HasError() {
return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags))
}
model.IPv6Nameservers = ipv6NameserversTF
}
if networkResp.PrefixesV6 == nil {
model.IPv6Prefixes = types.ListNull(types.StringType)
} else {
respPrefixesV6 := *networkResp.PrefixesV6
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
if diags.HasError() {
return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixesV6) > 0 {
model.IPv6Prefix = types.StringValue(respPrefixesV6[0])
_, netmask, err := net.ParseCIDR(respPrefixesV6[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv6PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv6PrefixLength = types.Int64Value(int64(ones))
}
}
model.IPv6Prefixes = prefixesV6TF
}
model.IPv6Gateway = types.StringNull()
if networkResp.Gatewayv6 != nil {
model.IPv6Gateway = types.StringPointerValue(networkResp.GetGatewayv6())
}
model.NetworkId = types.StringValue(networkId)
model.Name = types.StringPointerValue(networkResp.Name)
model.PublicIP = types.StringPointerValue(networkResp.PublicIp)
model.Labels = labels
model.Routed = types.BoolPointerValue(networkResp.Routed)
model.RoutingTableID = types.StringNull()
model.Region = types.StringNull()
return nil
}

View file

@ -1,352 +0,0 @@
package v1network
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
)
func TestMapDataSourceFields(t *testing.T) {
tests := []struct {
description string
state networkModel.DataSourceModel
input *iaas.Network
expected networkModel.DataSourceModel
isValid bool
}{
{
"id_ok",
networkModel.DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
Gateway: iaas.NewNullableString(nil),
},
networkModel.DataSourceModel{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
Nameservers: types.ListNull(types.StringType),
IPv4Nameservers: types.ListNull(types.StringType),
IPv4PrefixLength: types.Int64Null(),
IPv4Gateway: types.StringNull(),
IPv4Prefix: types.StringNull(),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Null(),
IPv6Gateway: types.StringNull(),
IPv6Prefix: types.StringNull(),
IPv6Prefixes: types.ListNull(types.StringType),
PublicIP: types.StringNull(),
Labels: types.MapNull(types.StringType),
Routed: types.BoolNull(),
},
true,
},
{
"values_ok",
networkModel.DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
Name: utils.Ptr("name"),
Nameservers: &[]string{
"ns1",
"ns2",
},
Prefixes: &[]string{
"192.168.42.0/24",
"10.100.10.0/16",
},
NameserversV6: &[]string{
"ns1",
"ns2",
},
PrefixesV6: &[]string{
"fd12:3456:789a:1::/64",
"fd12:3456:789a:2::/64",
},
PublicIp: utils.Ptr("publicIp"),
Labels: &map[string]interface{}{
"key": "value",
},
Routed: utils.Ptr(true),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")),
},
networkModel.DataSourceModel{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringValue("name"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv4PrefixLength: types.Int64Value(24),
Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("192.168.42.0/24"),
types.StringValue("10.100.10.0/16"),
}),
IPv4Prefix: types.StringValue("192.168.42.0/24"),
IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("192.168.42.0/24"),
types.StringValue("10.100.10.0/16"),
}),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv6PrefixLength: types.Int64Value(64),
IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"),
IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("fd12:3456:789a:1::/64"),
types.StringValue("fd12:3456:789a:2::/64"),
}),
PublicIP: types.StringValue("publicIp"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
IPv4Gateway: types.StringValue("gateway"),
IPv6Gateway: types.StringValue("gateway"),
},
true,
},
{
"ipv4_nameservers_changed_outside_tf",
networkModel.DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
Nameservers: &[]string{
"ns2",
"ns3",
},
},
networkModel.DataSourceModel{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
IPv6Prefixes: types.ListNull(types.StringType),
IPv6Nameservers: types.ListNull(types.StringType),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns2"),
types.StringValue("ns3"),
}),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns2"),
types.StringValue("ns3"),
}),
Labels: types.MapNull(types.StringType),
},
true,
},
{
"ipv6_nameservers_changed_outside_tf",
networkModel.DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
NameserversV6: &[]string{
"ns2",
"ns3",
},
},
networkModel.DataSourceModel{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
IPv6Prefixes: types.ListNull(types.StringType),
IPv4Nameservers: types.ListNull(types.StringType),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
Nameservers: types.ListNull(types.StringType),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns2"),
types.StringValue("ns3"),
}),
Labels: types.MapNull(types.StringType),
},
true,
},
{
"ipv4_prefixes_changed_outside_tf",
networkModel.DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("192.168.42.0/24"),
types.StringValue("10.100.10.0/16"),
}),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
Prefixes: &[]string{
"10.100.20.0/16",
"10.100.10.0/16",
},
},
networkModel.DataSourceModel{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Null(),
IPv6Prefixes: types.ListNull(types.StringType),
Labels: types.MapNull(types.StringType),
Nameservers: types.ListNull(types.StringType),
IPv4Nameservers: types.ListNull(types.StringType),
IPv4PrefixLength: types.Int64Value(16),
IPv4Prefix: types.StringValue("10.100.20.0/16"),
Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("10.100.20.0/16"),
types.StringValue("10.100.10.0/16"),
}),
IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("10.100.20.0/16"),
types.StringValue("10.100.10.0/16"),
}),
},
true,
},
{
"ipv6_prefixes_changed_outside_tf",
networkModel.DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("fd12:3456:789a:1::/64"),
types.StringValue("fd12:3456:789a:2::/64"),
}),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
PrefixesV6: &[]string{
"fd12:3456:789a:3::/64",
"fd12:3456:789a:4::/64",
},
},
networkModel.DataSourceModel{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
IPv4Nameservers: types.ListNull(types.StringType),
IPv4PrefixLength: types.Int64Null(),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
Labels: types.MapNull(types.StringType),
Nameservers: types.ListNull(types.StringType),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Value(64),
IPv6Prefix: types.StringValue("fd12:3456:789a:3::/64"),
IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("fd12:3456:789a:3::/64"),
types.StringValue("fd12:3456:789a:4::/64"),
}),
},
true,
},
{
"ipv4_ipv6_gateway_nil",
networkModel.DataSourceModel{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
},
networkModel.DataSourceModel{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
Nameservers: types.ListNull(types.StringType),
IPv4Nameservers: types.ListNull(types.StringType),
IPv4PrefixLength: types.Int64Null(),
IPv4Gateway: types.StringNull(),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Null(),
IPv6Gateway: types.StringNull(),
IPv6Prefixes: types.ListNull(types.StringType),
PublicIP: types.StringNull(),
Labels: types.MapNull(types.StringType),
Routed: types.BoolNull(),
},
true,
},
{
"response_nil_fail",
networkModel.DataSourceModel{},
nil,
networkModel.DataSourceModel{},
false,
},
{
"no_resource_id",
networkModel.DataSourceModel{
ProjectId: types.StringValue("pid"),
},
&iaas.Network{},
networkModel.DataSourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapDataSourceFields(context.Background(), tt.input, &tt.state)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}

View file

@ -1,558 +0,0 @@
package v1network
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)
func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model networkModel.Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create new network
network, err := client.CreateNetwork(ctx, projectId).CreateNetworkPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
networkId := *network.NetworkId
network, err = wait.CreateNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err))
return
}
ctx = tflog.SetField(ctx, "network_id", networkId)
// Map response body to schema
err = mapFields(ctx, network, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network created")
}
func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform
var model networkModel.Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
networkResp, err := client.GetNetwork(ctx, projectId, networkId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, networkResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network read")
}
func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model networkModel.Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
// Retrieve values from state
var stateModel networkModel.Model
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Generate API request body from model
payload, err := toUpdatePayload(ctx, &model, &stateModel)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Update existing network
err = client.PartialUpdateNetwork(ctx, projectId, networkId).PartialUpdateNetworkPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
waitResp, err := wait.UpdateNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err))
return
}
err = mapFields(ctx, waitResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network updated")
}
func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from state
var model networkModel.Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
// Delete existing network
err := client.DeleteNetwork(ctx, projectId, networkId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
_, err = wait.DeleteNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err))
return
}
tflog.Info(ctx, "Network deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,network_id
func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing network",
fmt.Sprintf("Expected import identifier with format: [project_id],[network_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
networkId := idParts[1]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...)
tflog.Info(ctx, "Network state imported")
}
func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkModel.Model) error {
if networkResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var networkId string
if model.NetworkId.ValueString() != "" {
networkId = model.NetworkId.ValueString()
} else if networkResp.NetworkId != nil {
networkId = *networkResp.NetworkId
} else {
return fmt.Errorf("network id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), networkId)
labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels)
if err != nil {
return err
}
// IPv4
if networkResp.Nameservers == nil {
model.Nameservers = types.ListNull(types.StringType)
model.IPv4Nameservers = types.ListNull(types.StringType)
} else {
respNameservers := *networkResp.Nameservers
modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers)
modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers)
if err != nil {
return fmt.Errorf("get current network nameservers from model: %w", err)
}
if errIpv4 != nil {
return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4)
}
reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers)
reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers)
nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers)
ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers)
if diags.HasError() {
return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags))
}
if ipv4Diags.HasError() {
return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags))
}
model.Nameservers = nameserversTF
model.IPv4Nameservers = ipv4NameserversTF
}
if networkResp.Prefixes == nil {
model.Prefixes = types.ListNull(types.StringType)
model.IPv4Prefixes = types.ListNull(types.StringType)
} else {
respPrefixes := *networkResp.Prefixes
prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes)
if diags.HasError() {
return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixes) > 0 {
model.IPv4Prefix = types.StringValue(respPrefixes[0])
_, netmask, err := net.ParseCIDR(respPrefixes[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv4PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv4PrefixLength = types.Int64Value(int64(ones))
}
}
model.Prefixes = prefixesTF
model.IPv4Prefixes = prefixesTF
}
if networkResp.Gateway != nil {
model.IPv4Gateway = types.StringPointerValue(networkResp.GetGateway())
} else {
model.IPv4Gateway = types.StringNull()
}
// IPv6
if networkResp.NameserversV6 == nil {
model.IPv6Nameservers = types.ListNull(types.StringType)
} else {
respIPv6Nameservers := *networkResp.NameserversV6
modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers)
if errIpv6 != nil {
return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6)
}
reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers)
ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers)
if ipv6Diags.HasError() {
return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags))
}
model.IPv6Nameservers = ipv6NameserversTF
}
if networkResp.PrefixesV6 == nil || len(*networkResp.PrefixesV6) == 0 {
model.IPv6Prefixes = types.ListNull(types.StringType)
model.IPv6Prefix = types.StringNull()
model.IPv6PrefixLength = types.Int64Null()
} else {
respPrefixesV6 := *networkResp.PrefixesV6
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
if diags.HasError() {
return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixesV6) > 0 {
model.IPv6Prefix = types.StringValue(respPrefixesV6[0])
_, netmask, err := net.ParseCIDR(respPrefixesV6[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv6PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv6PrefixLength = types.Int64Value(int64(ones))
}
}
model.IPv6Prefixes = prefixesV6TF
}
if networkResp.Gatewayv6 != nil {
model.IPv6Gateway = types.StringPointerValue(networkResp.GetGatewayv6())
} else {
model.IPv6Gateway = types.StringNull()
}
model.NetworkId = types.StringValue(networkId)
model.Name = types.StringPointerValue(networkResp.Name)
model.PublicIP = types.StringPointerValue(networkResp.PublicIp)
model.Labels = labels
model.Routed = types.BoolPointerValue(networkResp.Routed)
model.Region = types.StringNull()
model.RoutingTableID = types.StringNull()
return nil
}
func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.CreateNetworkPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
addressFamily := &iaas.CreateNetworkAddressFamily{}
var modelIPv6Nameservers []string
// Is true when IPv6Nameservers is not null or unset
if !utils.IsUndefined(model.IPv6Nameservers) {
// If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice.
// empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set
modelIPv6Nameservers = []string{}
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
ipv6NameserverString, ok := ipv6ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
}
}
if !utils.IsUndefined(model.IPv6Prefix) || !utils.IsUndefined(model.IPv6PrefixLength) || (modelIPv6Nameservers != nil) {
addressFamily.Ipv6 = &iaas.CreateNetworkIPv6Body{
Prefix: conversion.StringValueToPointer(model.IPv6Prefix),
PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength),
}
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
if modelIPv6Nameservers != nil {
addressFamily.Ipv6.Nameservers = &modelIPv6Nameservers
}
if model.NoIPv6Gateway.ValueBool() {
addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) {
addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
}
}
modelIPv4Nameservers := []string{}
var modelIPv4List []attr.Value
if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) {
modelIPv4List = model.IPv4Nameservers.Elements()
} else {
modelIPv4List = model.Nameservers.Elements()
}
for _, ipv4ns := range modelIPv4List {
ipv4NameserverString, ok := ipv4ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString())
}
if !model.IPv4Prefix.IsNull() || !model.IPv4PrefixLength.IsNull() || !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() {
addressFamily.Ipv4 = &iaas.CreateNetworkIPv4Body{
Nameservers: &modelIPv4Nameservers,
Prefix: conversion.StringValueToPointer(model.IPv4Prefix),
PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength),
}
if model.NoIPv4Gateway.ValueBool() {
addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) {
addressFamily.Ipv4.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway))
}
}
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
payload := iaas.CreateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
Routed: conversion.BoolValueToPointer(model.Routed),
}
if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil {
payload.AddressFamily = addressFamily
}
return &payload, nil
}
func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) (*iaas.PartialUpdateNetworkPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
addressFamily := &iaas.UpdateNetworkAddressFamily{}
var modelIPv6Nameservers []string
// Is true when IPv6Nameservers is not null or unset
if !utils.IsUndefined(model.IPv6Nameservers) {
// If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice.
// empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set
modelIPv6Nameservers = []string{}
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
ipv6NameserverString, ok := ipv6ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
}
}
if !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) || modelIPv6Nameservers != nil {
addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{}
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
if modelIPv6Nameservers != nil {
addressFamily.Ipv6.Nameservers = &modelIPv6Nameservers
}
if model.NoIPv6Gateway.ValueBool() {
addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil)
} else if !utils.IsUndefined(model.IPv6Gateway) {
addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
}
}
modelIPv4Nameservers := []string{}
var modelIPv4List []attr.Value
if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) {
modelIPv4List = model.IPv4Nameservers.Elements()
} else {
modelIPv4List = model.Nameservers.Elements()
}
for _, ipv4ns := range modelIPv4List {
ipv4NameserverString, ok := ipv4ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString())
}
if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() {
addressFamily.Ipv4 = &iaas.UpdateNetworkIPv4Body{
Nameservers: &modelIPv4Nameservers,
}
if model.NoIPv4Gateway.ValueBool() {
addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil)
} else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) {
addressFamily.Ipv4.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway))
}
}
currentLabels := stateModel.Labels
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
payload := iaas.PartialUpdateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
}
if addressFamily.Ipv6 != nil || addressFamily.Ipv4 != nil {
payload.AddressFamily = addressFamily
}
return &payload, nil
}

View file

@ -1,811 +0,0 @@
package v1network
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
)
func TestMapFields(t *testing.T) {
tests := []struct {
description string
state model.Model
input *iaas.Network
expected model.Model
isValid bool
}{
{
"id_ok",
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
Gateway: iaas.NewNullableString(nil),
},
model.Model{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
Nameservers: types.ListNull(types.StringType),
IPv4Nameservers: types.ListNull(types.StringType),
IPv4PrefixLength: types.Int64Null(),
IPv4Gateway: types.StringNull(),
IPv4Prefix: types.StringNull(),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Null(),
IPv6Gateway: types.StringNull(),
IPv6Prefix: types.StringNull(),
IPv6Prefixes: types.ListNull(types.StringType),
PublicIP: types.StringNull(),
Labels: types.MapNull(types.StringType),
Routed: types.BoolNull(),
},
true,
},
{
"values_ok",
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
Name: utils.Ptr("name"),
Nameservers: &[]string{
"ns1",
"ns2",
},
Prefixes: &[]string{
"192.168.42.0/24",
"10.100.10.0/16",
},
NameserversV6: &[]string{
"ns1",
"ns2",
},
PrefixesV6: &[]string{
"fd12:3456:789a:1::/64",
"fd12:3456:789b:1::/64",
},
PublicIp: utils.Ptr("publicIp"),
Labels: &map[string]interface{}{
"key": "value",
},
Routed: utils.Ptr(true),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Gatewayv6: iaas.NewNullableString(utils.Ptr("gateway")),
},
model.Model{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringValue("name"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv4PrefixLength: types.Int64Value(24),
Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("192.168.42.0/24"),
types.StringValue("10.100.10.0/16"),
}),
IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("192.168.42.0/24"),
types.StringValue("10.100.10.0/16"),
}),
IPv4Prefix: types.StringValue("192.168.42.0/24"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv6PrefixLength: types.Int64Value(64),
IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("fd12:3456:789a:1::/64"),
types.StringValue("fd12:3456:789b:1::/64"),
}),
IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"),
PublicIP: types.StringValue("publicIp"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
IPv4Gateway: types.StringValue("gateway"),
IPv6Gateway: types.StringValue("gateway"),
},
true,
},
{
"ipv4_nameservers_changed_outside_tf",
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
Nameservers: &[]string{
"ns2",
"ns3",
},
},
model.Model{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
IPv6Prefixes: types.ListNull(types.StringType),
IPv6Nameservers: types.ListNull(types.StringType),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns2"),
types.StringValue("ns3"),
}),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns2"),
types.StringValue("ns3"),
}),
Labels: types.MapNull(types.StringType),
},
true,
},
{
"ipv6_nameservers_changed_outside_tf",
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
NameserversV6: &[]string{
"ns2",
"ns3",
},
},
model.Model{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
IPv6Prefixes: types.ListNull(types.StringType),
IPv4Nameservers: types.ListNull(types.StringType),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
Nameservers: types.ListNull(types.StringType),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns2"),
types.StringValue("ns3"),
}),
Labels: types.MapNull(types.StringType),
},
true,
},
{
"ipv4_prefixes_changed_outside_tf",
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("192.168.42.0/24"),
types.StringValue("10.100.10.0/24"),
}),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
Prefixes: &[]string{
"192.168.54.0/24",
"192.168.55.0/24",
},
},
model.Model{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Null(),
IPv6Prefixes: types.ListNull(types.StringType),
Labels: types.MapNull(types.StringType),
Nameservers: types.ListNull(types.StringType),
IPv4Nameservers: types.ListNull(types.StringType),
IPv4PrefixLength: types.Int64Value(24),
IPv4Prefix: types.StringValue("192.168.54.0/24"),
Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("192.168.54.0/24"),
types.StringValue("192.168.55.0/24"),
}),
IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("192.168.54.0/24"),
types.StringValue("192.168.55.0/24"),
}),
},
true,
},
{
"ipv6_prefixes_changed_outside_tf",
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("fd12:3456:789a:1::/64"),
types.StringValue("fd12:3456:789a:2::/64"),
}),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
PrefixesV6: &[]string{
"fd12:3456:789a:1::/64",
"fd12:3456:789a:2::/64",
},
},
model.Model{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
IPv4Nameservers: types.ListNull(types.StringType),
IPv4PrefixLength: types.Int64Null(),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
Labels: types.MapNull(types.StringType),
Nameservers: types.ListNull(types.StringType),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Value(64),
IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("fd12:3456:789a:1::/64"),
types.StringValue("fd12:3456:789a:2::/64"),
}),
IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"),
},
true,
},
{
"ipv4_ipv6_gateway_nil",
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
},
&iaas.Network{
NetworkId: utils.Ptr("nid"),
},
model.Model{
Id: types.StringValue("pid,nid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Name: types.StringNull(),
Nameservers: types.ListNull(types.StringType),
IPv4Nameservers: types.ListNull(types.StringType),
IPv4PrefixLength: types.Int64Null(),
IPv4Gateway: types.StringNull(),
Prefixes: types.ListNull(types.StringType),
IPv4Prefixes: types.ListNull(types.StringType),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Null(),
IPv6Gateway: types.StringNull(),
IPv6Prefixes: types.ListNull(types.StringType),
PublicIP: types.StringNull(),
Labels: types.MapNull(types.StringType),
Routed: types.BoolNull(),
},
true,
},
{
"response_nil_fail",
model.Model{},
nil,
model.Model{},
false,
},
{
"no_resource_id",
model.Model{
ProjectId: types.StringValue("pid"),
},
&iaas.Network{},
model.Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *model.Model
expected *iaas.CreateNetworkPayload
isValid bool
}{
{
"default_ok",
&model.Model{
Name: types.StringValue("name"),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv4PrefixLength: types.Int64Value(24),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(false),
IPv4Gateway: types.StringValue("gateway"),
IPv4Prefix: types.StringValue("prefix"),
},
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.CreateNetworkAddressFamily{
Ipv4: &iaas.CreateNetworkIPv4Body{
Nameservers: &[]string{
"ns1",
"ns2",
},
PrefixLength: utils.Ptr(int64(24)),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Prefix: utils.Ptr("prefix"),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
Routed: utils.Ptr(false),
},
true,
},
{
"ipv4_nameservers_okay",
&model.Model{
Name: types.StringValue("name"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv4PrefixLength: types.Int64Value(24),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(false),
IPv4Gateway: types.StringValue("gateway"),
IPv4Prefix: types.StringValue("prefix"),
},
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.CreateNetworkAddressFamily{
Ipv4: &iaas.CreateNetworkIPv4Body{
Nameservers: &[]string{
"ns1",
"ns2",
},
PrefixLength: utils.Ptr(int64(24)),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Prefix: utils.Ptr("prefix"),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
Routed: utils.Ptr(false),
},
true,
},
{
"ipv6_default_ok",
&model.Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
IPv6PrefixLength: types.Int64Value(24),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(false),
IPv6Gateway: types.StringValue("gateway"),
IPv6Prefix: types.StringValue("prefix"),
},
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.CreateNetworkAddressFamily{
Ipv6: &iaas.CreateNetworkIPv6Body{
Nameservers: &[]string{
"ns1",
"ns2",
},
PrefixLength: utils.Ptr(int64(24)),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Prefix: utils.Ptr("prefix"),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
Routed: utils.Ptr(false),
},
true,
},
{
"ipv6_nameserver_null",
&model.Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListNull(types.StringType),
IPv6PrefixLength: types.Int64Value(24),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(false),
IPv6Gateway: types.StringValue("gateway"),
IPv6Prefix: types.StringValue("prefix"),
},
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.CreateNetworkAddressFamily{
Ipv6: &iaas.CreateNetworkIPv6Body{
Nameservers: nil,
PrefixLength: utils.Ptr(int64(24)),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Prefix: utils.Ptr("prefix"),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
Routed: utils.Ptr(false),
},
true,
},
{
"ipv6_nameserver_empty_list",
&model.Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}),
IPv6PrefixLength: types.Int64Value(24),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(false),
IPv6Gateway: types.StringValue("gateway"),
IPv6Prefix: types.StringValue("prefix"),
},
&iaas.CreateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.CreateNetworkAddressFamily{
Ipv6: &iaas.CreateNetworkIPv6Body{
Nameservers: utils.Ptr([]string{}),
PrefixLength: utils.Ptr(int64(24)),
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
Prefix: utils.Ptr("prefix"),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
Routed: utils.Ptr(false),
},
true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toCreatePayload(context.Background(), tt.input)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{}))
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestToUpdatePayload(t *testing.T) {
tests := []struct {
description string
input *model.Model
state model.Model
expected *iaas.PartialUpdateNetworkPayload
isValid bool
}{
{
"default_ok",
&model.Model{
Name: types.StringValue("name"),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
IPv4Gateway: types.StringValue("gateway"),
},
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv4: &iaas.UpdateNetworkIPv4Body{
Nameservers: &[]string{
"ns1",
"ns2",
},
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
},
true,
},
{
"ipv4_nameservers_okay",
&model.Model{
Name: types.StringValue("name"),
Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
IPv4Gateway: types.StringValue("gateway"),
},
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv4: &iaas.UpdateNetworkIPv4Body{
Nameservers: &[]string{
"ns1",
"ns2",
},
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
},
true,
},
{
"ipv4_gateway_nil",
&model.Model{
Name: types.StringValue("name"),
IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
},
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv4: &iaas.UpdateNetworkIPv4Body{
Nameservers: &[]string{
"ns1",
"ns2",
},
},
},
Labels: &map[string]interface{}{
"key": "value",
},
},
true,
},
{
"ipv6_default_ok",
&model.Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
IPv6Gateway: types.StringValue("gateway"),
},
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv6: &iaas.UpdateNetworkIPv6Body{
Nameservers: &[]string{
"ns1",
"ns2",
},
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
},
true,
},
{
"ipv6_gateway_nil",
&model.Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
},
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv6: &iaas.UpdateNetworkIPv6Body{
Nameservers: &[]string{
"ns1",
"ns2",
},
},
},
Labels: &map[string]interface{}{
"key": "value",
},
},
true,
},
{
"ipv6_nameserver_null",
&model.Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListNull(types.StringType),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
IPv6Gateway: types.StringValue("gateway"),
},
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv6: &iaas.UpdateNetworkIPv6Body{
Nameservers: nil,
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
},
true,
},
{
"ipv6_nameserver_empty_list",
&model.Model{
Name: types.StringValue("name"),
IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Routed: types.BoolValue(true),
IPv6Gateway: types.StringValue("gateway"),
},
model.Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
Labels: types.MapNull(types.StringType),
},
&iaas.PartialUpdateNetworkPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.UpdateNetworkAddressFamily{
Ipv6: &iaas.UpdateNetworkIPv6Body{
Nameservers: &[]string{},
Gateway: iaas.NewNullableString(utils.Ptr("gateway")),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
},
true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toUpdatePayload(context.Background(), tt.input, &tt.state)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{}))
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}

View file

@ -1,220 +0,0 @@
package v2network
import (
"context"
"fmt"
"net"
"net/http"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)
func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { // nolint:gocritic // function signature required by Terraform
var model networkModel.DataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := providerData.GetRegionWithOverride(model.Region)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
networkResp, err := client.GetNetwork(ctx, projectId, region, networkId).Execute()
if err != nil {
utils.LogError(
ctx,
&resp.Diagnostics,
err,
"Reading network",
fmt.Sprintf("Network with ID %q does not exist in project %q.", networkId, projectId),
map[int]string{
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
},
)
resp.State.RemoveResource(ctx)
return
}
ctx = core.LogResponse(ctx)
err = mapDataSourceFields(ctx, networkResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network read")
}
func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, model *networkModel.DataSourceModel, region string) error {
if networkResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var networkId string
if model.NetworkId.ValueString() != "" {
networkId = model.NetworkId.ValueString()
} else if networkResp.Id != nil {
networkId = *networkResp.Id
} else {
return fmt.Errorf("network id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId)
labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels)
if err != nil {
return err
}
// IPv4
if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil {
model.Nameservers = types.ListNull(types.StringType)
model.IPv4Nameservers = types.ListNull(types.StringType)
} else {
respNameservers := *networkResp.Ipv4.Nameservers
modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers)
modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers)
if err != nil {
return fmt.Errorf("get current network nameservers from model: %w", err)
}
if errIpv4 != nil {
return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4)
}
reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers)
reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers)
nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers)
ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers)
if diags.HasError() {
return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags))
}
if ipv4Diags.HasError() {
return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags))
}
model.Nameservers = nameserversTF
model.IPv4Nameservers = ipv4NameserversTF
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil {
model.Prefixes = types.ListNull(types.StringType)
model.IPv4Prefixes = types.ListNull(types.StringType)
} else {
respPrefixes := *networkResp.Ipv4.Prefixes
prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes)
if diags.HasError() {
return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixes) > 0 {
model.IPv4Prefix = types.StringValue(respPrefixes[0])
_, netmask, err := net.ParseCIDR(respPrefixes[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv4PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv4PrefixLength = types.Int64Value(int64(ones))
}
}
model.Prefixes = prefixesTF
model.IPv4Prefixes = prefixesTF
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil {
model.IPv4Gateway = types.StringNull()
} else {
model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway())
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil {
model.PublicIP = types.StringNull()
} else {
model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp)
}
// IPv6
if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil {
model.IPv6Nameservers = types.ListNull(types.StringType)
} else {
respIPv6Nameservers := *networkResp.Ipv6.Nameservers
modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers)
if errIpv6 != nil {
return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6)
}
reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers)
ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers)
if ipv6Diags.HasError() {
return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags))
}
model.IPv6Nameservers = ipv6NameserversTF
}
if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil {
model.IPv6Prefixes = types.ListNull(types.StringType)
} else {
respPrefixesV6 := *networkResp.Ipv6.Prefixes
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
if diags.HasError() {
return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixesV6) > 0 {
model.IPv6Prefix = types.StringValue(respPrefixesV6[0])
_, netmask, err := net.ParseCIDR(respPrefixesV6[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv6PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv6PrefixLength = types.Int64Value(int64(ones))
}
}
model.IPv6Prefixes = prefixesV6TF
}
if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil {
model.IPv6Gateway = types.StringNull()
} else {
model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway())
}
model.RoutingTableID = types.StringNull()
if networkResp.RoutingTableId != nil {
model.RoutingTableID = types.StringValue(*networkResp.RoutingTableId)
}
model.NetworkId = types.StringValue(networkId)
model.Name = types.StringPointerValue(networkResp.Name)
model.Labels = labels
model.Routed = types.BoolPointerValue(networkResp.Routed)
model.Region = types.StringValue(region)
return nil
}

View file

@ -1,603 +0,0 @@
package v2network
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
networkModel "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/model"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)
func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model networkModel.Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
region := model.Region.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create new network
network, err := client.CreateNetwork(ctx, projectId, region).CreateNetworkPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
networkId := *network.Id
network, err = wait.CreateNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err))
return
}
ctx = tflog.SetField(ctx, "network_id", networkId)
// Map response body to schema
err = mapFields(ctx, network, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network created")
}
func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { // nolint:gocritic // function signature required by Terraform
var model networkModel.Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := providerData.GetRegionWithOverride(model.Region)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "region", region)
networkResp, err := client.GetNetwork(ctx, projectId, region, networkId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, networkResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network read")
}
func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model networkModel.Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := model.Region.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "region", region)
// Retrieve values from state
var stateModel networkModel.Model
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Generate API request body from model
payload, err := toUpdatePayload(ctx, &model, &stateModel)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Update existing network
err = client.PartialUpdateNetwork(ctx, projectId, region, networkId).PartialUpdateNetworkPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
waitResp, err := wait.UpdateNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err))
return
}
err = mapFields(ctx, waitResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network updated")
}
func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from state
var model networkModel.Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
networkId := model.NetworkId.ValueString()
region := model.Region.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "region", region)
// Delete existing network
err := client.DeleteNetwork(ctx, projectId, region, networkId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
_, err = wait.DeleteNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err))
return
}
tflog.Info(ctx, "Network deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,region,network_id
func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing network",
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
region := idParts[1]
networkId := idParts[2]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_id", networkId)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...)
tflog.Info(ctx, "Network state imported")
}
func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *networkModel.Model, region string) error {
if networkResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var networkId string
if model.NetworkId.ValueString() != "" {
networkId = model.NetworkId.ValueString()
} else if networkResp.Id != nil {
networkId = *networkResp.Id
} else {
return fmt.Errorf("network id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId)
labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels)
if err != nil {
return err
}
// IPv4
if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil {
model.Nameservers = types.ListNull(types.StringType)
model.IPv4Nameservers = types.ListNull(types.StringType)
} else {
respNameservers := *networkResp.Ipv4.Nameservers
modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers)
modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers)
if err != nil {
return fmt.Errorf("get current network nameservers from model: %w", err)
}
if errIpv4 != nil {
return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4)
}
reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers)
reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers)
nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers)
ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers)
if diags.HasError() {
return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags))
}
if ipv4Diags.HasError() {
return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags))
}
model.Nameservers = nameserversTF
model.IPv4Nameservers = ipv4NameserversTF
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil {
model.Prefixes = types.ListNull(types.StringType)
model.IPv4Prefixes = types.ListNull(types.StringType)
} else {
respPrefixes := *networkResp.Ipv4.Prefixes
prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes)
if diags.HasError() {
return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixes) > 0 {
model.IPv4Prefix = types.StringValue(respPrefixes[0])
_, netmask, err := net.ParseCIDR(respPrefixes[0])
if err != nil {
tflog.Error(ctx, fmt.Sprintf("ipv4_prefix_length: %+v", err))
// silently ignore parsing error for the netmask
model.IPv4PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv4PrefixLength = types.Int64Value(int64(ones))
}
}
model.Prefixes = prefixesTF
model.IPv4Prefixes = prefixesTF
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil {
model.IPv4Gateway = types.StringNull()
} else {
model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway())
}
if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil {
model.PublicIP = types.StringNull()
} else {
model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp)
}
// IPv6
if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil {
model.IPv6Nameservers = types.ListNull(types.StringType)
} else {
respIPv6Nameservers := *networkResp.Ipv6.Nameservers
modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers)
if errIpv6 != nil {
return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6)
}
reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers)
ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers)
if ipv6Diags.HasError() {
return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags))
}
model.IPv6Nameservers = ipv6NameserversTF
}
if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil {
model.IPv6Prefixes = types.ListNull(types.StringType)
} else {
respPrefixesV6 := *networkResp.Ipv6.Prefixes
prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6)
if diags.HasError() {
return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags))
}
if len(respPrefixesV6) > 0 {
model.IPv6Prefix = types.StringValue(respPrefixesV6[0])
_, netmask, err := net.ParseCIDR(respPrefixesV6[0])
if err != nil {
// silently ignore parsing error for the netmask
model.IPv6PrefixLength = types.Int64Null()
} else {
ones, _ := netmask.Mask.Size()
model.IPv6PrefixLength = types.Int64Value(int64(ones))
}
}
model.IPv6Prefixes = prefixesV6TF
}
if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil {
model.IPv6Gateway = types.StringNull()
} else {
model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway())
}
if networkResp.RoutingTableId != nil {
model.RoutingTableID = types.StringPointerValue(networkResp.RoutingTableId)
} else {
model.RoutingTableID = types.StringNull()
}
model.NetworkId = types.StringValue(networkId)
model.Name = types.StringPointerValue(networkResp.Name)
model.Labels = labels
model.Routed = types.BoolPointerValue(networkResp.Routed)
model.Region = types.StringValue(region)
return nil
}
func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha.CreateNetworkPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
var modelIPv6Nameservers []string
// Is true when IPv6Nameservers is not null or unset
if !utils.IsUndefined(model.IPv6Nameservers) {
// If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice.
// empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set
modelIPv6Nameservers = []string{}
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
ipv6NameserverString, ok := ipv6ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
}
}
var ipv6Body *iaasalpha.CreateNetworkIPv6
if !utils.IsUndefined(model.IPv6PrefixLength) {
ipv6Body = &iaasalpha.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefixLength: &iaasalpha.CreateNetworkIPv6WithPrefixLength{
PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength),
},
}
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
if modelIPv6Nameservers != nil {
ipv6Body.CreateNetworkIPv6WithPrefixLength.Nameservers = &modelIPv6Nameservers
}
} else if !utils.IsUndefined(model.IPv6Prefix) {
var gateway *iaasalpha.NullableString
if model.NoIPv6Gateway.ValueBool() {
gateway = iaasalpha.NewNullableString(nil)
} else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) {
gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
}
ipv6Body = &iaasalpha.CreateNetworkIPv6{
CreateNetworkIPv6WithPrefix: &iaasalpha.CreateNetworkIPv6WithPrefix{
Gateway: gateway,
Prefix: conversion.StringValueToPointer(model.IPv6Prefix),
},
}
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
if modelIPv6Nameservers != nil {
ipv6Body.CreateNetworkIPv6WithPrefix.Nameservers = &modelIPv6Nameservers
}
}
modelIPv4Nameservers := []string{}
var modelIPv4List []attr.Value
if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) {
modelIPv4List = model.IPv4Nameservers.Elements()
} else {
modelIPv4List = model.Nameservers.Elements()
}
for _, ipv4ns := range modelIPv4List {
ipv4NameserverString, ok := ipv4ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString())
}
var ipv4Body *iaasalpha.CreateNetworkIPv4
if !utils.IsUndefined(model.IPv4PrefixLength) {
ipv4Body = &iaasalpha.CreateNetworkIPv4{
CreateNetworkIPv4WithPrefixLength: &iaasalpha.CreateNetworkIPv4WithPrefixLength{
Nameservers: &modelIPv4Nameservers,
PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength),
},
}
} else if !utils.IsUndefined(model.IPv4Prefix) {
var gateway *iaasalpha.NullableString
if model.NoIPv4Gateway.ValueBool() {
gateway = iaasalpha.NewNullableString(nil)
} else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) {
gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway))
}
ipv4Body = &iaasalpha.CreateNetworkIPv4{
CreateNetworkIPv4WithPrefix: &iaasalpha.CreateNetworkIPv4WithPrefix{
Nameservers: &modelIPv4Nameservers,
Prefix: conversion.StringValueToPointer(model.IPv4Prefix),
Gateway: gateway,
},
}
}
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
payload := iaasalpha.CreateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
Routed: conversion.BoolValueToPointer(model.Routed),
Ipv4: ipv4Body,
Ipv6: ipv6Body,
RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID),
}
return &payload, nil
}
func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) (*iaasalpha.PartialUpdateNetworkPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
var modelIPv6Nameservers []string
// Is true when IPv6Nameservers is not null or unset
if !utils.IsUndefined(model.IPv6Nameservers) {
// If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice.
// empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set
modelIPv6Nameservers = []string{}
for _, ipv6ns := range model.IPv6Nameservers.Elements() {
ipv6NameserverString, ok := ipv6ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString())
}
}
var ipv6Body *iaasalpha.UpdateNetworkIPv6Body
if modelIPv6Nameservers != nil || !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) {
ipv6Body = &iaasalpha.UpdateNetworkIPv6Body{}
// IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set.
// Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload,
// but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable.
if modelIPv6Nameservers != nil {
ipv6Body.Nameservers = &modelIPv6Nameservers
}
if model.NoIPv6Gateway.ValueBool() {
ipv6Body.Gateway = iaasalpha.NewNullableString(nil)
} else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) {
ipv6Body.Gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway))
}
}
modelIPv4Nameservers := []string{}
var modelIPv4List []attr.Value
if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) {
modelIPv4List = model.IPv4Nameservers.Elements()
} else {
modelIPv4List = model.Nameservers.Elements()
}
for _, ipv4ns := range modelIPv4List {
ipv4NameserverString, ok := ipv4ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString())
}
var ipv4Body *iaasalpha.UpdateNetworkIPv4Body
if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() {
ipv4Body = &iaasalpha.UpdateNetworkIPv4Body{
Nameservers: &modelIPv4Nameservers,
}
if model.NoIPv4Gateway.ValueBool() {
ipv4Body.Gateway = iaasalpha.NewNullableString(nil)
} else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) {
ipv4Body.Gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway))
}
}
currentLabels := stateModel.Labels
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
payload := iaasalpha.PartialUpdateNetworkPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
Ipv4: ipv4Body,
Ipv6: ipv6Body,
RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID),
}
return &payload, nil
}

View file

@ -2,9 +2,15 @@ package networkarea
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
@ -17,8 +23,6 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
@ -58,6 +62,7 @@ func (d *networkAreaDataSource) Configure(ctx context.Context, req datasource.Co
// Schema defines the schema for the data source.
func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
deprecationMsg := "Deprecated because of the IaaS API v1 -> v2 migration. Will be removed in May 2026."
description := "Network area datasource schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
Description: description,
@ -99,13 +104,15 @@ func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaReq
},
},
"default_nameservers": schema.ListAttribute{
Description: "List of DNS Servers/Nameservers.",
Computed: true,
ElementType: types.StringType,
DeprecationMessage: deprecationMsg,
Description: "List of DNS Servers/Nameservers.",
Computed: true,
ElementType: types.StringType,
},
"network_ranges": schema.ListNestedAttribute{
Description: "List of Network ranges.",
Computed: true,
DeprecationMessage: deprecationMsg,
Description: "List of Network ranges.",
Computed: true,
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
listvalidator.SizeAtMost(64),
@ -126,28 +133,32 @@ func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaReq
},
},
"transfer_network": schema.StringAttribute{
Description: "Classless Inter-Domain Routing (CIDR).",
Computed: true,
DeprecationMessage: deprecationMsg,
Description: "Classless Inter-Domain Routing (CIDR).",
Computed: true,
},
"default_prefix_length": schema.Int64Attribute{
Description: "The default prefix length for networks in the network area.",
Computed: true,
DeprecationMessage: deprecationMsg,
Description: "The default prefix length for networks in the network area.",
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(24),
int64validator.AtMost(29),
},
},
"max_prefix_length": schema.Int64Attribute{
Description: "The maximal prefix length for networks in the network area.",
Computed: true,
DeprecationMessage: deprecationMsg,
Description: "The maximal prefix length for networks in the network area.",
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(24),
int64validator.AtMost(29),
},
},
"min_prefix_length": schema.Int64Attribute{
Description: "The minimal prefix length for networks in the network area.",
Computed: true,
DeprecationMessage: deprecationMsg,
Description: "The minimal prefix length for networks in the network area.",
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(22),
int64validator.AtMost(29),
@ -196,13 +207,32 @@ func (d *networkAreaDataSource) Read(ctx context.Context, req datasource.ReadReq
ctx = core.LogResponse(ctx)
networkAreaRanges := networkAreaResp.Ipv4.NetworkRanges
err = mapFields(ctx, networkAreaResp, networkAreaRanges, &model)
err = mapFields(ctx, networkAreaResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
networkAreaRegionResp, err := d.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if !(ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest)) { // TODO: iaas api returns http 400 in case network area region is not found
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err))
return
}
networkAreaRegionResp = &iaas.RegionalArea{}
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {

View file

@ -2,6 +2,7 @@ package networkarea
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
@ -34,26 +35,55 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
const (
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
defaultValueDefaultPrefixLength = 25
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
defaultValueMinPrefixLength = 24
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
defaultValueMaxPrefixLength = 29
// Deprecated: Will be removed in May 2026.
deprecationWarningSummary = "Migration to new `stackit_network_area_region` resource needed"
// Deprecated: Will be removed in May 2026.
deprecationWarningDetails = "You're using deprecated features of the `stackit_network_area` resource. These will be removed in May 2026. Migrate to the new `stackit_network_area_region` resource instead."
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &networkAreaResource{}
_ resource.ResourceWithConfigure = &networkAreaResource{}
_ resource.ResourceWithImportState = &networkAreaResource{}
_ resource.Resource = &networkAreaResource{}
_ resource.ResourceWithConfigure = &networkAreaResource{}
_ resource.ResourceWithImportState = &networkAreaResource{}
_ resource.ResourceWithValidateConfig = &networkAreaResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
OrganizationId types.String `tfsdk:"organization_id"`
NetworkAreaId types.String `tfsdk:"network_area_id"`
Name types.String `tfsdk:"name"`
ProjectCount types.Int64 `tfsdk:"project_count"`
DefaultNameservers types.List `tfsdk:"default_nameservers"`
NetworkRanges types.List `tfsdk:"network_ranges"`
TransferNetwork types.String `tfsdk:"transfer_network"`
DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"`
MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"`
MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"`
Labels types.Map `tfsdk:"labels"`
Id types.String `tfsdk:"id"` // needed by TF
OrganizationId types.String `tfsdk:"organization_id"`
NetworkAreaId types.String `tfsdk:"network_area_id"`
Name types.String `tfsdk:"name"`
ProjectCount types.Int64 `tfsdk:"project_count"`
Labels types.Map `tfsdk:"labels"`
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
DefaultNameservers types.List `tfsdk:"default_nameservers"`
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"`
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
NetworkRanges types.List `tfsdk:"network_ranges"`
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
TransferNetwork types.String `tfsdk:"transfer_network"`
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"`
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"`
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. LegacyMode checks if any of the deprecated fields are set which now relate to the network area region API resource.
func (model *Model) LegacyMode() bool {
return !model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown() || !model.TransferNetwork.IsNull() || model.TransferNetwork.IsUnknown() || !model.DefaultNameservers.IsNull() || model.DefaultNameservers.IsUnknown() || model.DefaultPrefixLength != types.Int64Value(int64(defaultValueDefaultPrefixLength)) || model.MinPrefixLength != types.Int64Value(int64(defaultValueMinPrefixLength)) || model.MaxPrefixLength != types.Int64Value(int64(defaultValueMaxPrefixLength))
}
// Struct corresponding to Model.NetworkRanges[i]
@ -104,9 +134,27 @@ func (r *networkAreaResource) Configure(ctx context.Context, req resource.Config
tflog.Info(ctx, "IaaS client configured")
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
func (r *networkAreaResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var resourceModel Model
resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...)
if resp.Diagnostics.HasError() {
return
}
if resourceModel.NetworkRanges.IsNull() != resourceModel.TransferNetwork.IsNull() {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network network area", "You have to either provide both the `network_ranges` and `transfer_network` fields simultaneously or none of them.")
}
if (resourceModel.NetworkRanges.IsNull() || resourceModel.TransferNetwork.IsNull()) && (!resourceModel.DefaultNameservers.IsNull() || !resourceModel.DefaultPrefixLength.IsNull() || !resourceModel.MinPrefixLength.IsNull() || !resourceModel.MaxPrefixLength.IsNull()) {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network network area", "You have to provide both the `network_ranges` and `transfer_network` fields when providing one of these fields: `default_nameservers`, `default_prefix_length`, `max_prefix_length`, `min_prefix_length`")
}
}
// Schema defines the schema for the resource.
func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Network area resource schema. Must have a `region` specified in the provider configuration."
deprecationMsg := "Deprecated because of the IaaS API v1 -> v2 migration. Will be removed in May 2026. Use the new `stackit_network_area_region` resource instead."
description := "Network area resource schema."
resp.Schema = schema.Schema{
Description: description,
MarkdownDescription: description,
@ -155,14 +203,18 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest
int64validator.AtLeast(0),
},
},
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
"default_nameservers": schema.ListAttribute{
Description: "List of DNS Servers/Nameservers.",
Optional: true,
ElementType: types.StringType,
Description: "List of DNS Servers/Nameservers for configuration of network area for region `eu01`.",
DeprecationMessage: deprecationMsg,
Optional: true,
ElementType: types.StringType,
},
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
"network_ranges": schema.ListNestedAttribute{
Description: "List of Network ranges.",
Required: true,
Description: "List of Network ranges for configuration of network area for region `eu01`.",
DeprecationMessage: deprecationMsg,
Optional: true,
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
listvalidator.SizeAtMost(64),
@ -170,55 +222,65 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"network_range_id": schema.StringAttribute{
Computed: true,
DeprecationMessage: deprecationMsg,
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"prefix": schema.StringAttribute{
Description: "Classless Inter-Domain Routing (CIDR).",
Required: true,
DeprecationMessage: deprecationMsg,
Description: "Classless Inter-Domain Routing (CIDR).",
Required: true,
},
},
},
},
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
"transfer_network": schema.StringAttribute{
Description: "Classless Inter-Domain Routing (CIDR).",
Required: true,
DeprecationMessage: deprecationMsg,
Description: "Classless Inter-Domain Routing (CIDR) for configuration of network area for region `eu01`.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
"default_prefix_length": schema.Int64Attribute{
Description: "The default prefix length for networks in the network area.",
Optional: true,
Computed: true,
DeprecationMessage: deprecationMsg,
Description: "The default prefix length for networks in the network area for region `eu01`.",
Optional: true,
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(24),
int64validator.AtMost(29),
},
Default: int64default.StaticInt64(25),
Default: int64default.StaticInt64(defaultValueDefaultPrefixLength),
},
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
"max_prefix_length": schema.Int64Attribute{
Description: "The maximal prefix length for networks in the network area.",
Optional: true,
Computed: true,
DeprecationMessage: deprecationMsg,
Description: "The maximal prefix length for networks in the network area for region `eu01`.",
Optional: true,
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(24),
int64validator.AtMost(29),
},
Default: int64default.StaticInt64(29),
Default: int64default.StaticInt64(defaultValueMaxPrefixLength),
},
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
"min_prefix_length": schema.Int64Attribute{
Description: "The minimal prefix length for networks in the network area.",
Optional: true,
Computed: true,
DeprecationMessage: deprecationMsg,
Description: "The minimal prefix length for networks in the network area for region `eu01`.",
Optional: true,
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(8),
int64validator.AtMost(29),
},
Default: int64default.StaticInt64(24),
Default: int64default.StaticInt64(defaultValueMinPrefixLength),
},
"labels": schema.MapAttribute{
Description: "Labels are key-value string pairs which can be attached to a resource container",
@ -233,8 +295,7 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest
func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
@ -253,7 +314,7 @@ func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateReq
}
// Create new network area
area, err := r.client.CreateNetworkArea(ctx, organizationId).CreateNetworkAreaPayload(*payload).Execute()
networkArea, err := r.client.CreateNetworkArea(ctx, organizationId).CreateNetworkAreaPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Calling API: %v", err))
return
@ -261,25 +322,66 @@ func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateReq
ctx = core.LogResponse(ctx)
networkArea, err := wait.CreateNetworkAreaWaitHandler(ctx, r.client, organizationId, *area.AreaId).WaitWithContext(context.Background())
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Network area creation waiting: %v", err))
return
}
networkAreaId := *networkArea.AreaId
networkAreaId := *networkArea.Id
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
networkAreaRanges := networkArea.Ipv4.NetworkRanges
// Map response body to schema
err = mapFields(ctx, networkArea, networkAreaRanges, &model)
err = mapFields(ctx, networkArea, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
if model.LegacyMode() {
core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails)
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
regionCreatePayload, err := toRegionCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
networkAreaRegionCreateResp, err := r.client.CreateNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").CreateNetworkAreaRegionPayload(*regionCreatePayload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Calling API: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
err = mapNetworkAreaRegionFields(ctx, networkAreaRegionCreateResp, &model) // map partial state - just in case anything goes wrong during the wait handler
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
networkAreaRegionResp, err := wait.CreateNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, "eu01").WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for network area region creation", fmt.Sprintf("Calling API: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
} else {
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes})
model.DefaultNameservers = types.ListNull(types.StringType)
model.TransferNetwork = types.StringNull()
model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength)
model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength)
model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength)
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
@ -289,11 +391,11 @@ func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateReq
// Read refreshes the Terraform state with the latest data.
func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
@ -304,7 +406,8 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest
networkAreaResp, err := r.client.GetNetworkArea(ctx, organizationId, networkAreaId).Execute()
if err != nil {
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
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
@ -315,17 +418,53 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest
ctx = core.LogResponse(ctx)
networkAreaRanges := networkAreaResp.Ipv4.NetworkRanges
// Map response body to schema
err = mapFields(ctx, networkAreaResp, networkAreaRanges, &model)
err = mapFields(ctx, networkAreaResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
if model.LegacyMode() {
core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails)
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if !(ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest)) { // TODO: iaas api returns http 400 in case network area region is not found
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err))
return
}
model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes})
model.DefaultNameservers = types.ListNull(types.StringType)
model.TransferNetwork = types.StringNull()
model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength)
model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength)
model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength)
} else {
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
}
} else {
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes})
model.DefaultNameservers = types.ListNull(types.StringType)
model.TransferNetwork = types.StringNull()
model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength)
model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength)
model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength)
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
@ -336,11 +475,11 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest
func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
@ -351,8 +490,7 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq
ranges := []networkRange{}
if !(model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown()) {
diags = model.NetworkRanges.ElementsAs(ctx, &ranges, false)
resp.Diagnostics.Append(diags...)
resp.Diagnostics.Append(model.NetworkRanges.ElementsAs(ctx, &ranges, false)...)
if resp.Diagnostics.HasError() {
return
}
@ -360,8 +498,7 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq
// Retrieve values from state
var stateModel Model
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...)
if resp.Diagnostics.HasError() {
return
}
@ -373,7 +510,7 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq
return
}
// Update existing network
_, err = r.client.PartialUpdateNetworkArea(ctx, organizationId, networkAreaId).PartialUpdateNetworkAreaPayload(*payload).Execute()
networkAreaUpdateResp, err := r.client.PartialUpdateNetworkArea(ctx, organizationId, networkAreaId).PartialUpdateNetworkAreaPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Calling API: %v", err))
return
@ -381,39 +518,73 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq
ctx = core.LogResponse(ctx)
waitResp, err := wait.UpdateNetworkAreaWaitHandler(ctx, r.client, organizationId, networkAreaId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Network area update waiting: %v", err))
return
}
// Update network ranges
err = updateNetworkRanges(ctx, organizationId, networkAreaId, ranges, r.client)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Updating Network ranges: %v", err))
return
}
networkAreaResp, err := r.client.GetNetworkArea(ctx, organizationId, networkAreaId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Calling API: %v", err))
return
}
networkAreaRanges := networkAreaResp.Ipv4.NetworkRanges
err = mapFields(ctx, waitResp, networkAreaRanges, &model)
err = mapFields(ctx, networkAreaUpdateResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
if model.LegacyMode() {
core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails)
// Deprecated: Update network area region payload creation. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
regionUpdatePayload, err := toRegionUpdatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Deprecated: Update network area region. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
networkAreaRegionUpdateResp, err := r.client.UpdateNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").UpdateNetworkAreaRegionPayload(*regionUpdatePayload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err))
return
}
// Deprecated: Update network area region. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
err = mapNetworkAreaRegionFields(ctx, networkAreaRegionUpdateResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Deprecated: Update network ranges. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
err = updateNetworkRanges(ctx, organizationId, networkAreaId, ranges, r.client)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Updating Network ranges: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest) { // TODO: iaas api returns http 400 in case network area region is not found
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err))
return
}
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
} else {
// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes})
model.DefaultNameservers = types.ListNull(types.StringType)
model.TransferNetwork = types.StringNull()
model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength)
model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength)
model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength)
}
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
@ -444,7 +615,29 @@ func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteReq
return
}
// Delete existing network
// Get all configured regions so we can delete them one by one before deleting the network area
regionsListResp, err := r.client.ListNetworkAreaRegions(ctx, organizationId, networkAreaId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API to list configured regions: %v", err))
return
}
// Delete network region configurations
for region := range *regionsListResp.Regions {
err = r.client.DeleteNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API: %v", err))
return
}
_, err = wait.DeleteNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Waiting for networea deletion: %v", err))
return
}
}
// Delete existing network area
err = r.client.DeleteNetworkArea(ctx, organizationId, networkAreaId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Calling API: %v", err))
@ -453,12 +646,6 @@ func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteReq
ctx = core.LogResponse(ctx)
_, err = wait.DeleteNetworkAreaWaitHandler(ctx, r.client, organizationId, networkAreaId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Network area deletion waiting: %v", err))
return
}
tflog.Info(ctx, "Network area deleted")
}
@ -485,7 +672,7 @@ func (r *networkAreaResource) ImportState(ctx context.Context, req resource.Impo
tflog.Info(ctx, "Network state imported")
}
func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAreaRangesResp *[]iaas.NetworkRange, model *Model) error {
func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, model *Model) error {
if networkAreaResp == nil {
return fmt.Errorf("response input is nil")
}
@ -496,18 +683,41 @@ func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAr
var networkAreaId string
if model.NetworkAreaId.ValueString() != "" {
networkAreaId = model.NetworkAreaId.ValueString()
} else if networkAreaResp.AreaId != nil {
networkAreaId = *networkAreaResp.AreaId
} else if networkAreaResp.Id != nil {
networkAreaId = *networkAreaResp.Id
} else {
return fmt.Errorf("network area id not present")
}
model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), networkAreaId)
if networkAreaResp.Ipv4 == nil || networkAreaResp.Ipv4.DefaultNameservers == nil {
labels, err := iaasUtils.MapLabels(ctx, networkAreaResp.Labels, model.Labels)
if err != nil {
return err
}
model.NetworkAreaId = types.StringValue(networkAreaId)
model.Name = types.StringPointerValue(networkAreaResp.Name)
model.ProjectCount = types.Int64PointerValue(networkAreaResp.ProjectCount)
model.Labels = labels
return nil
}
// Deprecated: mapRegionFields maps the region configuration for eu01 to avoid a breaking change in the Terraform provider during the IaaS v1 -> v2 API migration. Will be removed in May 2026.
func mapNetworkAreaRegionFields(ctx context.Context, networkAreaRegionResp *iaas.RegionalArea, model *Model) error {
if model == nil {
return fmt.Errorf("model input is nil")
}
if networkAreaRegionResp == nil {
return fmt.Errorf("response input is nil")
}
// map default nameservers
if networkAreaRegionResp.Ipv4 == nil || networkAreaRegionResp.Ipv4.DefaultNameservers == nil {
model.DefaultNameservers = types.ListNull(types.StringType)
} else {
respDefaultNameservers := *networkAreaResp.Ipv4.DefaultNameservers
respDefaultNameservers := *networkAreaRegionResp.Ipv4.DefaultNameservers
modelDefaultNameservers, err := utils.ListValuetoStringSlice(model.DefaultNameservers)
if err != nil {
return fmt.Errorf("get current network area default nameservers from model: %w", err)
@ -523,31 +733,28 @@ func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAr
model.DefaultNameservers = defaultNameserversTF
}
err := mapNetworkRanges(ctx, networkAreaRangesResp, model)
if err != nil {
return fmt.Errorf("mapping network ranges: %w", err)
// map network ranges
if networkAreaRegionResp.Ipv4 == nil || networkAreaRegionResp.Ipv4.NetworkRanges == nil {
model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes})
} else {
err := mapNetworkRanges(ctx, networkAreaRegionResp.Ipv4.NetworkRanges, model)
if err != nil {
return fmt.Errorf("mapping network ranges: %w", err)
}
}
labels, err := iaasUtils.MapLabels(ctx, networkAreaResp.Labels, model.Labels)
if err != nil {
return err
}
model.NetworkAreaId = types.StringValue(networkAreaId)
model.Name = types.StringPointerValue(networkAreaResp.Name)
model.ProjectCount = types.Int64PointerValue(networkAreaResp.ProjectCount)
model.Labels = labels
if networkAreaResp.Ipv4 != nil {
model.TransferNetwork = types.StringPointerValue(networkAreaResp.Ipv4.TransferNetwork)
model.DefaultPrefixLength = types.Int64PointerValue(networkAreaResp.Ipv4.DefaultPrefixLen)
model.MaxPrefixLength = types.Int64PointerValue(networkAreaResp.Ipv4.MaxPrefixLen)
model.MinPrefixLength = types.Int64PointerValue(networkAreaResp.Ipv4.MinPrefixLen)
// map remaining fields
if networkAreaRegionResp.Ipv4 != nil {
model.TransferNetwork = types.StringPointerValue(networkAreaRegionResp.Ipv4.TransferNetwork)
model.DefaultPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.DefaultPrefixLen)
model.MaxPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.MaxPrefixLen)
model.MinPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.MinPrefixLen)
}
return nil
}
// Deprecated: mapNetworkRanges will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only kept to circumvent breaking changes.
func mapNetworkRanges(ctx context.Context, networkAreaRangesList *[]iaas.NetworkRange, model *Model) error {
var diags diag.Diagnostics
@ -584,7 +791,7 @@ func mapNetworkRanges(ctx context.Context, networkAreaRangesList *[]iaas.Network
var networkRangeId string
for _, networkRangeElement := range *networkAreaRangesList {
if *networkRangeElement.Prefix == prefix {
networkRangeId = *networkRangeElement.NetworkRangeId
networkRangeId = *networkRangeElement.Id
break
}
}
@ -618,13 +825,26 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea
return nil, fmt.Errorf("nil model")
}
modelDefaultNameservers := []string{}
for _, ns := range model.DefaultNameservers.Elements() {
nameserverString, ok := ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString())
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
return &iaas.CreateNetworkAreaPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
}, nil
}
// Deprecated: toRegionCreatePayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes.
func toRegionCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRegionPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model)
if err != nil {
return nil, fmt.Errorf("converting default nameservers: %w", err)
}
networkRangesPayload, err := toNetworkRangesPayload(ctx, model)
@ -632,24 +852,15 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea
return nil, fmt.Errorf("converting network ranges: %w", err)
}
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
return &iaas.CreateNetworkAreaPayload{
Name: conversion.StringValueToPointer(model.Name),
AddressFamily: &iaas.CreateAreaAddressFamily{
Ipv4: &iaas.CreateAreaIPv4{
DefaultNameservers: &modelDefaultNameservers,
NetworkRanges: networkRangesPayload,
TransferNetwork: conversion.StringValueToPointer(model.TransferNetwork),
DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength),
MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength),
MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength),
},
return &iaas.CreateNetworkAreaRegionPayload{
Ipv4: &iaas.RegionalAreaIPv4{
DefaultNameservers: &modelDefaultNameservers,
DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength),
MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength),
MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength),
TransferNetwork: conversion.StringValueToPointer(model.TransferNetwork),
NetworkRanges: networkRangesPayload,
},
Labels: &labels,
}, nil
}
@ -658,6 +869,40 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map)
return nil, fmt.Errorf("nil model")
}
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
return &iaas.PartialUpdateNetworkAreaPayload{
Name: conversion.StringValueToPointer(model.Name),
Labels: &labels,
}, nil
}
// Deprecated: toRegionUpdatePayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes.
func toRegionUpdatePayload(ctx context.Context, model *Model) (*iaas.UpdateNetworkAreaRegionPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model)
if err != nil {
return nil, fmt.Errorf("converting default nameservers: %w", err)
}
return &iaas.UpdateNetworkAreaRegionPayload{
Ipv4: &iaas.UpdateRegionalAreaIPv4{
DefaultNameservers: &modelDefaultNameservers,
DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength),
MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength),
MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength),
},
}, nil
}
// Deprecated: toDefaultNameserversPayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes.
func toDefaultNameserversPayload(_ context.Context, model *Model) ([]string, error) {
modelDefaultNameservers := []string{}
for _, ns := range model.DefaultNameservers.Elements() {
nameserverString, ok := ns.(types.String)
@ -667,25 +912,10 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map)
modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString())
}
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
return &iaas.PartialUpdateNetworkAreaPayload{
Name: conversion.StringValueToPointer(model.Name),
AddressFamily: &iaas.UpdateAreaAddressFamily{
Ipv4: &iaas.UpdateAreaIPv4{
DefaultNameservers: &modelDefaultNameservers,
DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength),
MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength),
MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength),
},
},
Labels: &labels,
}, nil
return modelDefaultNameservers, nil
}
// Deprecated: toNetworkRangesPayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes.
func toNetworkRangesPayload(ctx context.Context, model *Model) (*[]iaas.NetworkRange, error) {
if model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown() {
return nil, nil
@ -712,10 +942,10 @@ func toNetworkRangesPayload(ctx context.Context, model *Model) (*[]iaas.NetworkR
return &payload, nil
}
// updateNetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model
// Deprecated: updateNetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model. This was only kept to make the v1 -> v2 IaaS API migration non-breaking in the Terraform provider.
func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId string, ranges []networkRange, client *iaas.APIClient) error {
// Get network ranges current state
currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId).Execute()
currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId, "eu01").Execute()
if err != nil {
return fmt.Errorf("error reading network area ranges: %w", err)
}
@ -739,13 +969,13 @@ func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId stri
networkRangesState[prefix] = &networkRangeState{}
}
networkRangesState[prefix].isCreated = true
networkRangesState[prefix].id = *networkRange.NetworkRangeId
networkRangesState[prefix].id = *networkRange.Id
}
// Delete network ranges
for prefix, state := range networkRangesState {
if !state.isInModel && state.isCreated {
err := client.DeleteNetworkAreaRange(ctx, organizationId, networkAreaId, state.id).Execute()
err := client.DeleteNetworkAreaRange(ctx, organizationId, networkAreaId, "eu01", state.id).Execute()
if err != nil {
return fmt.Errorf("deleting network area range '%v': %w", prefix, err)
}
@ -763,7 +993,7 @@ func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId stri
},
}
_, err := client.CreateNetworkAreaRange(ctx, organizationId, networkAreaId).CreateNetworkAreaRangePayload(payload).Execute()
_, err := client.CreateNetworkAreaRange(ctx, organizationId, networkAreaId, "eu01").CreateNetworkAreaRangePayload(payload).Execute()
if err != nil {
return fmt.Errorf("creating network range '%v': %w", prefix, err)
}

View file

@ -28,16 +28,15 @@ var testRangeId2Repeated = uuid.NewString()
func TestMapFields(t *testing.T) {
tests := []struct {
description string
state Model
input *iaas.NetworkArea
ListNetworkRanges *[]iaas.NetworkRange
expected Model
isValid bool
description string
state Model
input *iaas.NetworkArea
expected Model
isValid bool
}{
{
"id_ok",
Model{
description: "id_ok",
state: Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
@ -50,32 +49,16 @@ func TestMapFields(t *testing.T) {
"prefix": types.StringValue("prefix-2"),
}),
}),
DefaultNameservers: types.ListNull(types.StringType),
},
&iaas.NetworkArea{
AreaId: utils.Ptr("naid"),
Ipv4: &iaas.NetworkAreaIPv4{},
input: &iaas.NetworkArea{
Id: utils.Ptr("naid"),
},
&[]iaas.NetworkRange{
{
NetworkRangeId: utils.Ptr(testRangeId1),
Prefix: utils.Ptr("prefix-1"),
},
{
NetworkRangeId: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
},
Model{
Id: types.StringValue("oid,naid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
Name: types.StringNull(),
DefaultNameservers: types.ListNull(types.StringType),
TransferNetwork: types.StringNull(),
DefaultPrefixLength: types.Int64Null(),
MaxPrefixLength: types.Int64Null(),
MinPrefixLength: types.Int64Null(),
expected: Model{
Id: types.StringValue("oid,naid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
Name: types.StringNull(),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
@ -86,13 +69,14 @@ func TestMapFields(t *testing.T) {
"prefix": types.StringValue("prefix-2"),
}),
}),
Labels: types.MapNull(types.StringType),
DefaultNameservers: types.ListNull(types.StringType),
Labels: types.MapNull(types.StringType),
},
true,
isValid: true,
},
{
"values_ok",
Model{
description: "values_ok",
state: Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
@ -105,47 +89,20 @@ func TestMapFields(t *testing.T) {
"prefix": types.StringValue("prefix-2"),
}),
}),
DefaultNameservers: types.ListNull(types.StringType),
},
&iaas.NetworkArea{
AreaId: utils.Ptr("naid"),
Ipv4: &iaas.NetworkAreaIPv4{
DefaultNameservers: &[]string{
"nameserver1",
"nameserver2",
},
TransferNetwork: utils.Ptr("network"),
DefaultPrefixLen: utils.Ptr(int64(20)),
MaxPrefixLen: utils.Ptr(int64(22)),
MinPrefixLen: utils.Ptr(int64(18)),
},
input: &iaas.NetworkArea{
Id: utils.Ptr("naid"),
Name: utils.Ptr("name"),
Labels: &map[string]interface{}{
"key": "value",
},
},
&[]iaas.NetworkRange{
{
NetworkRangeId: utils.Ptr(testRangeId1),
Prefix: utils.Ptr("prefix-1"),
},
{
NetworkRangeId: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
},
Model{
expected: Model{
Id: types.StringValue("oid,naid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
Name: types.StringValue("name"),
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("nameserver1"),
types.StringValue("nameserver2"),
}),
TransferNetwork: types.StringValue("network"),
DefaultPrefixLength: types.Int64Value(20),
MaxPrefixLength: types.Int64Value(22),
MinPrefixLength: types.Int64Value(18),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
@ -159,207 +116,53 @@ func TestMapFields(t *testing.T) {
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
},
true,
},
{
"model and response have ranges in different order",
Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
},
&iaas.NetworkArea{
AreaId: utils.Ptr("naid"),
Ipv4: &iaas.NetworkAreaIPv4{
DefaultNameservers: &[]string{
"nameserver1",
"nameserver2",
},
TransferNetwork: utils.Ptr("network"),
DefaultPrefixLen: utils.Ptr(int64(20)),
MaxPrefixLen: utils.Ptr(int64(22)),
MinPrefixLen: utils.Ptr(int64(18)),
},
Name: utils.Ptr("name"),
},
&[]iaas.NetworkRange{
{
NetworkRangeId: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
{
NetworkRangeId: utils.Ptr(testRangeId3),
Prefix: utils.Ptr("prefix-3"),
},
{
NetworkRangeId: utils.Ptr(testRangeId1),
Prefix: utils.Ptr("prefix-1"),
},
},
Model{
Id: types.StringValue("oid,naid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
Name: types.StringValue("name"),
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("nameserver1"),
types.StringValue("nameserver2"),
}),
TransferNetwork: types.StringValue("network"),
DefaultPrefixLength: types.Int64Value(20),
MaxPrefixLength: types.Int64Value(22),
MinPrefixLength: types.Int64Value(18),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId3),
"prefix": types.StringValue("prefix-3"),
}),
}),
Labels: types.MapNull(types.StringType),
},
true,
},
{
"default_nameservers_changed_outside_tf",
Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
},
&iaas.NetworkArea{
AreaId: utils.Ptr("naid"),
Ipv4: &iaas.NetworkAreaIPv4{
DefaultNameservers: &[]string{
"ns2",
"ns3",
},
},
},
&[]iaas.NetworkRange{
{
NetworkRangeId: utils.Ptr(testRangeId1),
Prefix: utils.Ptr("prefix-1"),
},
{
NetworkRangeId: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
},
Model{
Id: types.StringValue("oid,naid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns2"),
types.StringValue("ns3"),
}),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
Labels: types.MapNull(types.StringType),
},
true,
},
{
"network_ranges_changed_outside_tf",
Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
},
&iaas.NetworkArea{
AreaId: utils.Ptr("naid"),
Ipv4: &iaas.NetworkAreaIPv4{},
},
&[]iaas.NetworkRange{
{
NetworkRangeId: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
{
NetworkRangeId: utils.Ptr(testRangeId3),
Prefix: utils.Ptr("prefix-3"),
},
},
Model{
Id: types.StringValue("oid,naid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
DefaultNameservers: types.ListNull(types.StringType),
},
isValid: true,
},
{
description: "default_nameservers_changed_outside_tf",
state: Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
DefaultNameservers: types.ListNull(types.StringType),
},
input: &iaas.NetworkArea{
Id: utils.Ptr("naid"),
},
expected: Model{
Id: types.StringValue("oid,naid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId3),
"prefix": types.StringValue("prefix-3"),
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
Labels: types.MapNull(types.StringType),
Labels: types.MapNull(types.StringType),
DefaultNameservers: types.ListNull(types.StringType),
},
true,
},
{
"nil_network_ranges_list",
Model{},
&iaas.NetworkArea{},
nil,
Model{},
false,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
nil,
Model{},
false,
},
@ -369,14 +172,13 @@ func TestMapFields(t *testing.T) {
OrganizationId: types.StringValue("oid"),
},
&iaas.NetworkArea{},
&[]iaas.NetworkRange{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, tt.ListNetworkRanges, &tt.state)
err := mapFields(context.Background(), tt.input, &tt.state)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -393,6 +195,243 @@ func TestMapFields(t *testing.T) {
}
}
// Deprecated: Will be removed in May 2026.
func Test_MapNetworkRanges(t *testing.T) {
type args struct {
networkAreaRangesList *[]iaas.NetworkRange
model *Model
}
tests := []struct {
name string
args args
want *Model
wantErr bool
}{
{
name: "model and response have ranges in different order",
args: args{
model: &Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
DefaultNameservers: types.ListNull(types.StringType),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
Labels: types.MapNull(types.StringType),
},
networkAreaRangesList: &[]iaas.NetworkRange{
{
Id: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
{
Id: utils.Ptr(testRangeId3),
Prefix: utils.Ptr("prefix-3"),
},
{
Id: utils.Ptr(testRangeId1),
Prefix: utils.Ptr("prefix-1"),
},
},
},
want: &Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId3),
"prefix": types.StringValue("prefix-3"),
}),
}),
Labels: types.MapNull(types.StringType),
DefaultNameservers: types.ListNull(types.StringType),
},
wantErr: false,
},
{
name: "network_ranges_changed_outside_tf",
args: args{
model: &Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
Labels: types.MapNull(types.StringType),
DefaultNameservers: types.ListNull(types.StringType),
},
networkAreaRangesList: &[]iaas.NetworkRange{
{
Id: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
{
Id: utils.Ptr(testRangeId3),
Prefix: utils.Ptr("prefix-3"),
},
},
},
want: &Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId3),
"prefix": types.StringValue("prefix-3"),
}),
}),
Labels: types.MapNull(types.StringType),
DefaultNameservers: types.ListNull(types.StringType),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := mapNetworkRanges(context.Background(), tt.args.networkAreaRangesList, tt.args.model); (err != nil) != tt.wantErr {
t.Errorf("mapNetworkRanges() error = %v, wantErr %v", err, tt.wantErr)
}
diff := cmp.Diff(tt.args.model, tt.want)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
// Deprecated: Will be removed in May 2026.
func TestMapNetworkAreaRegionFields(t *testing.T) {
type args struct {
networkAreaRegionResp *iaas.RegionalArea
model *Model
}
tests := []struct {
name string
args args
want *Model
wantErr bool
}{
{
name: "default",
args: args{
model: &Model{
Labels: types.MapNull(types.StringType),
},
networkAreaRegionResp: &iaas.RegionalArea{
Ipv4: &iaas.RegionalAreaIPv4{
DefaultNameservers: &[]string{
"nameserver1",
"nameserver2",
},
TransferNetwork: utils.Ptr("network"),
DefaultPrefixLen: utils.Ptr(int64(20)),
MaxPrefixLen: utils.Ptr(int64(22)),
MinPrefixLen: utils.Ptr(int64(18)),
NetworkRanges: &[]iaas.NetworkRange{
{
Id: utils.Ptr(testRangeId1),
Prefix: utils.Ptr("prefix-1"),
},
{
Id: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("prefix-2"),
},
},
},
},
},
want: &Model{
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("nameserver1"),
types.StringValue("nameserver2"),
}),
TransferNetwork: types.StringValue("network"),
DefaultPrefixLength: types.Int64Value(20),
MaxPrefixLength: types.Int64Value(22),
MinPrefixLength: types.Int64Value(18),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId1),
"prefix": types.StringValue("prefix-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringValue(testRangeId2),
"prefix": types.StringValue("prefix-2"),
}),
}),
Labels: types.MapNull(types.StringType),
},
wantErr: false,
},
{
name: "model is nil",
args: args{
model: nil,
networkAreaRegionResp: &iaas.RegionalArea{},
},
want: nil,
wantErr: true,
},
{
name: "network area region response is nil",
args: args{
model: &Model{
DefaultNameservers: types.ListNull(types.StringType),
NetworkRanges: types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}),
Labels: types.MapNull(types.StringType),
},
networkAreaRegionResp: nil,
},
want: &Model{
DefaultNameservers: types.ListNull(types.StringType),
NetworkRanges: types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}),
Labels: types.MapNull(types.StringType),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := mapNetworkAreaRegionFields(context.Background(), tt.args.networkAreaRegionResp, tt.args.model); (err != nil) != tt.wantErr {
t.Errorf("mapNetworkAreaRegionFields() error = %v, wantErr %v", err, tt.wantErr)
}
diff := cmp.Diff(tt.args.model, tt.want)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
@ -404,50 +443,12 @@ func TestToCreatePayload(t *testing.T) {
"default_ok",
&Model{
Name: types.StringValue("name"),
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringUnknown(),
"prefix": types.StringValue("pr-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringUnknown(),
"prefix": types.StringValue("pr-2"),
}),
}),
TransferNetwork: types.StringValue("network"),
DefaultPrefixLength: types.Int64Value(20),
MaxPrefixLength: types.Int64Value(22),
MinPrefixLength: types.Int64Value(18),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
},
&iaas.CreateNetworkAreaPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.CreateAreaAddressFamily{
Ipv4: &iaas.CreateAreaIPv4{
DefaultNameservers: &[]string{
"ns1",
"ns2",
},
NetworkRanges: &[]iaas.NetworkRange{
{
Prefix: utils.Ptr("pr-1"),
},
{
Prefix: utils.Ptr("pr-2"),
},
},
TransferNetwork: utils.Ptr("network"),
DefaultPrefixLen: utils.Ptr(int64(20)),
MaxPrefixLen: utils.Ptr(int64(22)),
MinPrefixLen: utils.Ptr(int64(18)),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
@ -474,6 +475,86 @@ func TestToCreatePayload(t *testing.T) {
}
}
// Deprecated: Will be removed in May 2026.
func TestToRegionCreatePayload(t *testing.T) {
type args struct {
model *Model
}
tests := []struct {
name string
args args
want *iaas.CreateNetworkAreaRegionPayload
wantErr bool
}{
{
name: "default_ok",
args: args{
model: &Model{
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringUnknown(),
"prefix": types.StringValue("pr-1"),
}),
types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{
"network_range_id": types.StringUnknown(),
"prefix": types.StringValue("pr-2"),
}),
}),
TransferNetwork: types.StringValue("network"),
DefaultPrefixLength: types.Int64Value(20),
MaxPrefixLength: types.Int64Value(22),
MinPrefixLength: types.Int64Value(18),
},
},
want: &iaas.CreateNetworkAreaRegionPayload{
Ipv4: &iaas.RegionalAreaIPv4{
DefaultNameservers: &[]string{
"ns1",
"ns2",
},
NetworkRanges: &[]iaas.NetworkRange{
{
Prefix: utils.Ptr("pr-1"),
},
{
Prefix: utils.Ptr("pr-2"),
},
},
TransferNetwork: utils.Ptr("network"),
DefaultPrefixLen: utils.Ptr(int64(20)),
MaxPrefixLen: utils.Ptr(int64(22)),
MinPrefixLen: utils.Ptr(int64(18)),
},
},
},
{
name: "model is nil",
args: args{
model: nil,
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toRegionCreatePayload(context.Background(), tt.args.model)
if (err != nil) != tt.wantErr {
t.Errorf("toRegionCreatePayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
diff := cmp.Diff(got, tt.want)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestToUpdatePayload(t *testing.T) {
tests := []struct {
description string
@ -485,30 +566,12 @@ func TestToUpdatePayload(t *testing.T) {
"default_ok",
&Model{
Name: types.StringValue("name"),
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
DefaultPrefixLength: types.Int64Value(22),
MaxPrefixLength: types.Int64Value(24),
MinPrefixLength: types.Int64Value(20),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
},
&iaas.PartialUpdateNetworkAreaPayload{
Name: utils.Ptr("name"),
AddressFamily: &iaas.UpdateAreaAddressFamily{
Ipv4: &iaas.UpdateAreaIPv4{
DefaultNameservers: &[]string{
"ns1",
"ns2",
},
DefaultPrefixLen: utils.Ptr(int64(22)),
MaxPrefixLen: utils.Ptr(int64(24)),
MinPrefixLen: utils.Ptr(int64(20)),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
@ -535,24 +598,84 @@ func TestToUpdatePayload(t *testing.T) {
}
}
// Deprecated: Will be removed in May 2026.
func TestToRegionUpdatePayload(t *testing.T) {
type args struct {
model *Model
}
tests := []struct {
name string
args args
want *iaas.UpdateNetworkAreaRegionPayload
wantErr bool
}{
{
name: "default_ok",
args: args{
model: &Model{
DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("ns1"),
types.StringValue("ns2"),
}),
DefaultPrefixLength: types.Int64Value(22),
MaxPrefixLength: types.Int64Value(24),
MinPrefixLength: types.Int64Value(20),
},
},
want: &iaas.UpdateNetworkAreaRegionPayload{
Ipv4: &iaas.UpdateRegionalAreaIPv4{
DefaultNameservers: &[]string{
"ns1",
"ns2",
},
DefaultPrefixLen: utils.Ptr(int64(22)),
MaxPrefixLen: utils.Ptr(int64(24)),
MinPrefixLen: utils.Ptr(int64(20)),
},
},
},
{
name: "model is nil",
args: args{
model: nil,
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toRegionUpdatePayload(context.Background(), tt.args.model)
if (err != nil) != tt.wantErr {
t.Errorf("toRegionUpdatePayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
diff := cmp.Diff(got, tt.want)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
func TestUpdateNetworkRanges(t *testing.T) {
getAllNetworkRangesResp := iaas.NetworkRangeListResponse{
Items: &[]iaas.NetworkRange{
{
Prefix: utils.Ptr("pr-1"),
NetworkRangeId: utils.Ptr(testRangeId1),
Prefix: utils.Ptr("pr-1"),
Id: utils.Ptr(testRangeId1),
},
{
Prefix: utils.Ptr("pr-2"),
NetworkRangeId: utils.Ptr(testRangeId2),
Prefix: utils.Ptr("pr-2"),
Id: utils.Ptr(testRangeId2),
},
{
Prefix: utils.Ptr("pr-3"),
NetworkRangeId: utils.Ptr(testRangeId3),
Prefix: utils.Ptr("pr-3"),
Id: utils.Ptr(testRangeId3),
},
{
Prefix: utils.Ptr("pr-2"),
NetworkRangeId: utils.Ptr(testRangeId2Repeated),
Prefix: utils.Ptr("pr-2"),
Id: utils.Ptr(testRangeId2Repeated),
},
},
}
@ -903,8 +1026,8 @@ func TestUpdateNetworkRanges(t *testing.T) {
}
resp := iaas.NetworkRange{
Prefix: utils.Ptr("prefix"),
NetworkRangeId: utils.Ptr("id-range"),
Prefix: utils.Ptr("prefix"),
Id: utils.Ptr("id-range"),
}
respBytes, err := json.Marshal(resp)
if err != nil {
@ -930,7 +1053,7 @@ func TestUpdateNetworkRanges(t *testing.T) {
var prefix string
for _, rangeItem := range *getAllNetworkRangesResp.Items {
if *rangeItem.NetworkRangeId == networkRangeId {
if *rangeItem.Id == networkRangeId {
prefix = *rangeItem.Prefix
}
}
@ -963,14 +1086,14 @@ func TestUpdateNetworkRanges(t *testing.T) {
// Setup server and client
router := mux.NewRouter()
router.HandleFunc("/v1/organizations/{organizationId}/network-areas/{areaId}/network-ranges", func(w http.ResponseWriter, r *http.Request) {
router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges", func(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
getAllNetworkRangesHandler(w, r)
} else if r.Method == "POST" {
createNetworkRangeHandler(w, r)
}
})
router.HandleFunc("/v1/organizations/{organizationId}/network-areas/{areaId}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler)
router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler)
mockedServer := httptest.NewServer(router)
defer mockedServer.Close()
client, err := iaas.NewAPIClient(

View file

@ -0,0 +1,181 @@
package networkarearegion
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &networkAreaRegionDataSource{}
)
// NewNetworkAreaRegionDataSource is a helper function to simplify the provider implementation.
func NewNetworkAreaRegionDataSource() datasource.DataSource {
return &networkAreaRegionDataSource{}
}
// networkAreaRegionDataSource is the data source implementation.
type networkAreaRegionDataSource struct {
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
func (d *networkAreaRegionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_network_area_region"
}
func (d *networkAreaRegionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
d.client = apiClient
tflog.Info(ctx, "iaas client configured")
}
// Schema defines the schema for the resource.
func (d *networkAreaRegionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "Network area region data source schema."
resp.Schema = schema.Schema{
MarkdownDescription: description,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`\".",
Computed: true,
},
"organization_id": schema.StringAttribute{
Description: "STACKIT organization ID to which the network area is associated.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"network_area_id": schema.StringAttribute{
Description: "The network area ID.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"ipv4": schema.SingleNestedAttribute{
Computed: true,
Description: "The regional IPv4 config of a network area.",
Attributes: map[string]schema.Attribute{
"default_nameservers": schema.ListAttribute{
Description: "List of DNS Servers/Nameservers.",
Computed: true,
ElementType: types.StringType,
},
"network_ranges": schema.ListNestedAttribute{
Description: "List of Network ranges.",
Computed: true,
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
listvalidator.SizeAtMost(64),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"network_range_id": schema.StringAttribute{
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"prefix": schema.StringAttribute{
Description: "Classless Inter-Domain Routing (CIDR).",
Computed: true,
},
},
},
},
"transfer_network": schema.StringAttribute{
Description: "IPv4 Classless Inter-Domain Routing (CIDR).",
Computed: true,
},
"default_prefix_length": schema.Int64Attribute{
Description: "The default prefix length for networks in the network area.",
Computed: true,
},
"max_prefix_length": schema.Int64Attribute{
Description: "The maximal prefix length for networks in the network area.",
Computed: true,
},
"min_prefix_length": schema.Int64Attribute{
Description: "The minimal prefix length for networks in the network area.",
Computed: true,
},
},
},
},
}
}
// Read refreshes the Terraform state with the latest data.
func (d *networkAreaRegionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
networkAreaRegionResp, err := d.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute()
if err != nil {
utils.LogError(ctx, &resp.Diagnostics, err, "Reading network area region", fmt.Sprintf("Region configuration for %q for network area %q does not exist.", region, networkAreaId), nil)
resp.State.RemoveResource(ctx)
return
}
// Map response body to schema
err = mapFields(ctx, networkAreaRegionResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set refreshed state
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network area region read")
}

View file

@ -0,0 +1,728 @@
package networkarearegion
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/stackitcloud/stackit-sdk-go/services/resourcemanager"
resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils"
sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework/resource"
"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"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &networkAreaRegionResource{}
_ resource.ResourceWithConfigure = &networkAreaRegionResource{}
_ resource.ResourceWithImportState = &networkAreaRegionResource{}
_ resource.ResourceWithModifyPlan = &networkAreaRegionResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
OrganizationId types.String `tfsdk:"organization_id"`
NetworkAreaId types.String `tfsdk:"network_area_id"`
Region types.String `tfsdk:"region"`
Ipv4 *ipv4Model `tfsdk:"ipv4"`
}
// Struct corresponding to Model.Ipv4
type ipv4Model struct {
DefaultNameservers types.List `tfsdk:"default_nameservers"`
NetworkRanges []networkRangeModel `tfsdk:"network_ranges"`
TransferNetwork types.String `tfsdk:"transfer_network"`
DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"`
MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"`
MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"`
}
// Struct corresponding to Model.NetworkRanges[i]
type networkRangeModel struct {
Prefix types.String `tfsdk:"prefix"`
NetworkRangeId types.String `tfsdk:"network_range_id"`
}
// NewNetworkAreaRegionResource is a helper function to simplify the provider implementation.
func NewNetworkAreaRegionResource() resource.Resource {
return &networkAreaRegionResource{}
}
// networkAreaRegionResource is the resource implementation.
type networkAreaRegionResource struct {
client *iaas.APIClient
resourceManagerClient *resourcemanager.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
func (r *networkAreaRegionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_network_area_region"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *networkAreaRegionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *networkAreaRegionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
r.client = iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
r.resourceManagerClient = resourcemanagerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "iaas client configured")
}
// Schema defines the schema for the resource.
func (r *networkAreaRegionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Network area region resource schema."
resp.Schema = schema.Schema{
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"organization_id": schema.StringAttribute{
Description: "STACKIT organization ID to which the network area is associated.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"network_area_id": schema.StringAttribute{
Description: "The network area ID.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"ipv4": schema.SingleNestedAttribute{
Description: "The regional IPv4 config of a network area.",
Required: true,
Attributes: map[string]schema.Attribute{
"default_nameservers": schema.ListAttribute{
Description: "List of DNS Servers/Nameservers.",
Optional: true,
ElementType: types.StringType,
},
"network_ranges": schema.ListNestedAttribute{
Description: "List of Network ranges.",
Required: true,
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
listvalidator.SizeAtMost(64),
},
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"network_range_id": schema.StringAttribute{
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"prefix": schema.StringAttribute{
Description: "Classless Inter-Domain Routing (CIDR).",
Required: true,
},
},
},
},
"transfer_network": schema.StringAttribute{
Description: "IPv4 Classless Inter-Domain Routing (CIDR).",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"default_prefix_length": schema.Int64Attribute{
Description: "The default prefix length for networks in the network area.",
Optional: true,
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(24),
int64validator.AtMost(29),
},
Default: int64default.StaticInt64(25),
},
"max_prefix_length": schema.Int64Attribute{
Description: "The maximal prefix length for networks in the network area.",
Optional: true,
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(24),
int64validator.AtMost(29),
},
Default: int64default.StaticInt64(29),
},
"min_prefix_length": schema.Int64Attribute{
Description: "The minimal prefix length for networks in the network area.",
Optional: true,
Computed: true,
Validators: []validator.Int64{
int64validator.AtLeast(8),
int64validator.AtMost(29),
},
Default: int64default.StaticInt64(24),
},
},
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *networkAreaRegionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
ctx = core.InitProviderContext(ctx)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create new network area region configuration
networkAreaRegion, err := r.client.CreateNetworkAreaRegion(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRegionPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"organization_id": organizationId,
"network_area_id": networkAreaId,
"region": region,
})
// wait for creation of network area region to complete
_, err = wait.CreateNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err))
return
}
// Map response body to schema
err = mapFields(ctx, networkAreaRegion, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set state to fully populated data
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network area region created")
}
// Read refreshes the Terraform state with the latest data.
func (r *networkAreaRegionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
ctx = core.InitProviderContext(ctx)
networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
}
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, networkAreaRegionResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set refreshed state
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network area region read")
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *networkAreaRegionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
ctx = core.InitProviderContext(ctx)
// Retrieve values from state
var stateModel Model
resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...)
if resp.Diagnostics.HasError() {
return
}
// Generate API request body from model
payload, err := toUpdatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Update existing network area region configuration
_, err = r.client.UpdateNetworkAreaRegion(ctx, organizationId, networkAreaId, region).UpdateNetworkAreaRegionPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
err = updateIpv4NetworkRanges(ctx, organizationId, networkAreaId, model.Ipv4.NetworkRanges, r.client, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Updating Network ranges: %v", err))
return
}
updatedNetworkAreaRegion, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFields(ctx, updatedNetworkAreaRegion, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Processing API payload: %v", err))
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "network area region updated")
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *networkAreaRegionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from state
var model Model
resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
_, err := wait.ReadyForNetworkAreaDeletionWaitHandler(ctx, r.client, r.resourceManagerClient, organizationId, networkAreaId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Network area ready for deletion waiting: %v", err))
return
}
ctx = core.InitProviderContext(ctx)
// Delete network area region configuration
err = r.client.DeleteNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = core.LogResponse(ctx)
_, err = wait.DeleteNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("network area deletion waiting: %v", err))
return
}
tflog.Info(ctx, "Network area region deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: organization_id,network_area_id,region
func (r *networkAreaRegionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing network area region",
fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[region] Got: %q", req.ID),
)
return
}
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"organization_id": idParts[0],
"network_area_id": idParts[1],
"region": idParts[2],
})
tflog.Info(ctx, "Network area region state imported")
}
// mapFields maps the API response values to the Terraform resource model fields
func mapFields(ctx context.Context, networkAreaRegion *iaas.RegionalArea, model *Model, region string) error {
if networkAreaRegion == nil {
return fmt.Errorf("network are region input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), model.NetworkAreaId.ValueString(), region)
model.Region = types.StringValue(region)
model.Ipv4 = &ipv4Model{}
if networkAreaRegion.Ipv4 != nil {
model.Ipv4.TransferNetwork = types.StringPointerValue(networkAreaRegion.Ipv4.TransferNetwork)
model.Ipv4.DefaultPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.DefaultPrefixLen)
model.Ipv4.MaxPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.MaxPrefixLen)
model.Ipv4.MinPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.MinPrefixLen)
}
// map default nameservers
if networkAreaRegion.Ipv4 == nil || networkAreaRegion.Ipv4.DefaultNameservers == nil {
model.Ipv4.DefaultNameservers = types.ListNull(types.StringType)
} else {
respDefaultNameservers := *networkAreaRegion.Ipv4.DefaultNameservers
modelDefaultNameservers, err := utils.ListValuetoStringSlice(model.Ipv4.DefaultNameservers)
if err != nil {
return fmt.Errorf("get current network area default nameservers from model: %w", err)
}
reconciledDefaultNameservers := utils.ReconcileStringSlices(modelDefaultNameservers, respDefaultNameservers)
defaultNameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledDefaultNameservers)
if diags.HasError() {
return fmt.Errorf("map network area default nameservers: %w", core.DiagsToError(diags))
}
model.Ipv4.DefaultNameservers = defaultNameserversTF
}
// map network ranges
err := mapIpv4NetworkRanges(ctx, networkAreaRegion.Ipv4.NetworkRanges, model)
if err != nil {
return fmt.Errorf("mapping network ranges: %w", err)
}
return nil
}
// mapFields maps the API ipv4 network ranges response values to the Terraform resource model fields
func mapIpv4NetworkRanges(_ context.Context, networkAreaRangesList *[]iaas.NetworkRange, model *Model) error {
if networkAreaRangesList == nil {
return fmt.Errorf("nil network area ranges list")
}
if len(*networkAreaRangesList) == 0 {
model.Ipv4.NetworkRanges = []networkRangeModel{}
return nil
}
modelNetworkRangePrefixes := []string{}
for _, m := range model.Ipv4.NetworkRanges {
modelNetworkRangePrefixes = append(modelNetworkRangePrefixes, m.Prefix.ValueString())
}
apiNetworkRangePrefixes := []string{}
for _, n := range *networkAreaRangesList {
apiNetworkRangePrefixes = append(apiNetworkRangePrefixes, *n.Prefix)
}
reconciledRangePrefixes := utils.ReconcileStringSlices(modelNetworkRangePrefixes, apiNetworkRangePrefixes)
model.Ipv4.NetworkRanges = []networkRangeModel{}
for _, prefix := range reconciledRangePrefixes {
var networkRangeId string
for _, networkRangeElement := range *networkAreaRangesList {
if *networkRangeElement.Prefix == prefix {
networkRangeId = *networkRangeElement.Id
break
}
}
model.Ipv4.NetworkRanges = append(model.Ipv4.NetworkRanges, networkRangeModel{
Prefix: types.StringValue(prefix),
NetworkRangeId: types.StringValue(networkRangeId),
})
}
return nil
}
func toDefaultNameserversPayload(_ context.Context, model *Model) ([]string, error) {
if model == nil {
return nil, fmt.Errorf("model is nil")
}
modelDefaultNameservers := []string{}
for _, ns := range model.Ipv4.DefaultNameservers.Elements() {
nameserverString, ok := ns.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString())
}
return modelDefaultNameservers, nil
}
func toNetworkRangesPayload(_ context.Context, model *Model) (*[]iaas.NetworkRange, error) {
if model == nil {
return nil, fmt.Errorf("model is nil")
}
if len(model.Ipv4.NetworkRanges) == 0 {
return nil, nil
}
payload := []iaas.NetworkRange{}
for _, networkRange := range model.Ipv4.NetworkRanges {
payload = append(payload, iaas.NetworkRange{
Prefix: conversion.StringValueToPointer(networkRange.Prefix),
})
}
return &payload, nil
}
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRegionPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
} else if model.Ipv4 == nil {
return nil, fmt.Errorf("nil model.Ipv4")
}
modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model)
if err != nil {
return nil, fmt.Errorf("converting default nameservers: %w", err)
}
networkRangesPayload, err := toNetworkRangesPayload(ctx, model)
if err != nil {
return nil, fmt.Errorf("converting network ranges: %w", err)
}
return &iaas.CreateNetworkAreaRegionPayload{
Ipv4: &iaas.RegionalAreaIPv4{
DefaultNameservers: &modelDefaultNameservers,
DefaultPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.DefaultPrefixLength),
MaxPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MaxPrefixLength),
MinPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MinPrefixLength),
TransferNetwork: conversion.StringValueToPointer(model.Ipv4.TransferNetwork),
NetworkRanges: networkRangesPayload,
},
}, nil
}
func toUpdatePayload(ctx context.Context, model *Model) (*iaas.UpdateNetworkAreaRegionPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model)
if err != nil {
return nil, fmt.Errorf("converting default nameservers: %w", err)
}
return &iaas.UpdateNetworkAreaRegionPayload{
Ipv4: &iaas.UpdateRegionalAreaIPv4{
DefaultNameservers: &modelDefaultNameservers,
DefaultPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.DefaultPrefixLength),
MaxPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MaxPrefixLength),
MinPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MinPrefixLength),
},
}, nil
}
// updateIpv4NetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model.
func updateIpv4NetworkRanges(ctx context.Context, organizationId, networkAreaId string, ranges []networkRangeModel, client *iaas.APIClient, region string) error {
// Get network ranges current state
currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId, region).Execute()
if err != nil {
return fmt.Errorf("error reading network area ranges: %w", err)
}
type networkRangeState struct {
isInModel bool
isCreated bool
id string
}
networkRangesState := make(map[string]*networkRangeState)
for _, nwRange := range ranges {
networkRangesState[nwRange.Prefix.ValueString()] = &networkRangeState{
isInModel: true,
}
}
for _, networkRange := range *currentNetworkRangesResp.Items {
prefix := *networkRange.Prefix
if _, ok := networkRangesState[prefix]; !ok {
networkRangesState[prefix] = &networkRangeState{}
}
networkRangesState[prefix].isCreated = true
networkRangesState[prefix].id = *networkRange.Id
}
// Delete network ranges
for prefix, state := range networkRangesState {
if !state.isInModel && state.isCreated {
err := client.DeleteNetworkAreaRange(ctx, organizationId, networkAreaId, region, state.id).Execute()
if err != nil {
return fmt.Errorf("deleting network area range '%v': %w", prefix, err)
}
}
}
// Create network ranges
for prefix, state := range networkRangesState {
if state.isInModel && !state.isCreated {
payload := iaas.CreateNetworkAreaRangePayload{
Ipv4: &[]iaas.NetworkRange{
{
Prefix: sdkUtils.Ptr(prefix),
},
},
}
_, err := client.CreateNetworkAreaRange(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRangePayload(payload).Execute()
if err != nil {
return fmt.Errorf("creating network range '%v': %w", prefix, err)
}
}
}
return nil
}

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,8 @@ func NewNetworkAreaRouteDataSource() datasource.DataSource {
// networkDataSource is the data source implementation.
type networkAreaRouteDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -40,12 +41,13 @@ func (d *networkAreaRouteDataSource) Metadata(_ context.Context, req datasource.
}
func (d *networkAreaRouteDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -61,7 +63,7 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche
MarkdownDescription: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal data source ID. It is structured as \"`organization_id`,`network_area_id`,`network_area_route_id`\".",
Description: "Terraform's internal data source ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`network_area_route_id`\".",
Computed: true,
},
"organization_id": schema.StringAttribute{
@ -80,6 +82,11 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"network_area_route_id": schema.StringAttribute{
Description: "The network area route ID.",
Required: true,
@ -88,13 +95,33 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche
validate.NoSeparator(),
},
},
"next_hop": schema.StringAttribute{
Description: "The IP address of the routing system, that will route the prefix configured. Should be a valid IPv4 address.",
"destination": schema.SingleNestedAttribute{
Description: "Destination of the route.",
Computed: true,
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Description: fmt.Sprintf("CIDRV type. %s", utils.FormatPossibleValues("cidrv4", "cidrv6")),
Computed: true,
},
"value": schema.StringAttribute{
Description: "An CIDR string.",
Computed: true,
},
},
},
"prefix": schema.StringAttribute{
Description: "The network, that is reachable though the Next Hop. Should use CIDR notation.",
"next_hop": schema.SingleNestedAttribute{
Description: "Next hop destination.",
Computed: true,
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Description: "Type of the next hop. " + utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"),
Computed: true,
},
"value": schema.StringAttribute{
Description: "Either IPv4 or IPv6 (not set for blackhole and internet).",
Computed: true,
},
},
},
"labels": schema.MapAttribute{
Description: "Labels are key-value string pairs which can be attached to a resource container",
@ -107,23 +134,26 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche
// Read refreshes the Terraform state with the latest data.
func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
var model ModelV1
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
networkAreaRouteId := model.NetworkAreaRouteId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId)
networkAreaRouteResp, err := d.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).Execute()
networkAreaRouteResp, err := d.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute()
if err != nil {
utils.LogError(
ctx,
@ -141,11 +171,12 @@ func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.Re
ctx = core.LogResponse(ctx)
err = mapFields(ctx, networkAreaRouteResp, &model)
err = mapFields(ctx, networkAreaRouteResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {

View file

@ -6,11 +6,12 @@ import (
"net/http"
"strings"
sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@ -27,13 +28,28 @@ import (
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &networkAreaRouteResource{}
_ resource.ResourceWithConfigure = &networkAreaRouteResource{}
_ resource.ResourceWithImportState = &networkAreaRouteResource{}
_ resource.Resource = &networkAreaRouteResource{}
_ resource.ResourceWithConfigure = &networkAreaRouteResource{}
_ resource.ResourceWithImportState = &networkAreaRouteResource{}
_ resource.ResourceWithModifyPlan = &networkAreaRouteResource{}
_ resource.ResourceWithUpgradeState = &networkAreaRouteResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
// ModelV1 is the currently used model
type ModelV1 struct {
Id types.String `tfsdk:"id"` // needed by TF
OrganizationId types.String `tfsdk:"organization_id"`
Region types.String `tfsdk:"region"`
NetworkAreaId types.String `tfsdk:"network_area_id"`
NetworkAreaRouteId types.String `tfsdk:"network_area_route_id"`
NextHop *NexthopModelV1 `tfsdk:"next_hop"`
Destination *DestinationModelV1 `tfsdk:"destination"`
Labels types.Map `tfsdk:"labels"`
}
// ModelV0 is the old model (only needed for state upgrade)
type ModelV0 struct {
Id types.String `tfsdk:"id"`
OrganizationId types.String `tfsdk:"organization_id"`
NetworkAreaId types.String `tfsdk:"network_area_id"`
NetworkAreaRouteId types.String `tfsdk:"network_area_route_id"`
@ -42,6 +58,18 @@ type Model struct {
Labels types.Map `tfsdk:"labels"`
}
// DestinationModelV1 maps the route destination data
type DestinationModelV1 struct {
Type types.String `tfsdk:"type"`
Value types.String `tfsdk:"value"`
}
// NexthopModelV1 maps the route nexthop data
type NexthopModelV1 struct {
Type types.String `tfsdk:"type"`
Value types.String `tfsdk:"value"`
}
// NewNetworkAreaRouteResource is a helper function to simplify the provider implementation.
func NewNetworkAreaRouteResource() resource.Resource {
return &networkAreaRouteResource{}
@ -49,7 +77,8 @@ func NewNetworkAreaRouteResource() resource.Resource {
// networkResource is the resource implementation.
type networkAreaRouteResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -57,14 +86,45 @@ func (r *networkAreaRouteResource) Metadata(_ context.Context, req resource.Meta
resp.TypeName = req.ProviderTypeName + "_network_area_route"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *networkAreaRouteResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel ModelV1
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel ModelV1
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *networkAreaRouteResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -78,9 +138,10 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe
resp.Schema = schema.Schema{
Description: description,
MarkdownDescription: description,
Version: 1,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`network_area_route_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`,`network_area_route_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -97,6 +158,15 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"network_area_id": schema.StringAttribute{
Description: "The network area ID to which the network area route is associated.",
Required: true,
@ -121,24 +191,50 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe
validate.NoSeparator(),
},
},
"next_hop": schema.StringAttribute{
Description: "The IP address of the routing system, that will route the prefix configured. Should be a valid IPv4 address.",
"next_hop": schema.SingleNestedAttribute{
Description: "Next hop destination.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.IP(false),
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Description: fmt.Sprintf("Type of the next hop. %s %s", utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"), "Only `ipv4` supported currently."),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"value": schema.StringAttribute{
Description: "Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported currently.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.IP(false),
},
},
},
},
"prefix": schema.StringAttribute{
Description: "The network, that is reachable though the Next Hop. Should use CIDR notation.",
"destination": schema.SingleNestedAttribute{
Description: "Destination of the route.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.CIDR(),
Attributes: map[string]schema.Attribute{
"type": schema.StringAttribute{
Description: fmt.Sprintf("CIDRV type. %s %s", utils.FormatPossibleValues("cidrv4", "cidrv6"), "Only `cidrv4` is supported currently."),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"value": schema.StringAttribute{
Description: "An CIDR string.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.CIDR(),
},
},
},
},
"labels": schema.MapAttribute{
@ -150,10 +246,91 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe
}
}
func (r *networkAreaRouteResource) UpgradeState(_ context.Context) map[int64]resource.StateUpgrader {
return map[int64]resource.StateUpgrader{
0: {
// This handles moving from version 0 to 1
PriorSchema: &schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
},
"organization_id": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"network_area_id": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"network_area_route_id": schema.StringAttribute{
Computed: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"next_hop": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validate.IP(false),
},
},
"prefix": schema.StringAttribute{
Required: true,
Validators: []validator.String{
validate.CIDR(),
},
},
"labels": schema.MapAttribute{
ElementType: types.StringType,
Optional: true,
},
},
},
StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) {
var priorStateData ModelV0
resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...)
if resp.Diagnostics.HasError() {
return
}
nexthopValue := priorStateData.NextHop.ValueString()
prefixValue := priorStateData.Prefix.ValueString()
newStateData := ModelV1{
Id: priorStateData.Id,
OrganizationId: priorStateData.OrganizationId,
NetworkAreaId: priorStateData.NetworkAreaId,
NetworkAreaRouteId: priorStateData.NetworkAreaRouteId,
Labels: priorStateData.Labels,
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue(nexthopValue),
},
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue(prefixValue),
},
}
resp.Diagnostics.Append(resp.State.Set(ctx, newStateData)...)
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
var model ModelV1
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -163,8 +340,10 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea
ctx = core.InitProviderContext(ctx)
organizationId := model.OrganizationId.ValueString()
ctx = tflog.SetField(ctx, "organization_id", organizationId)
region := r.providerData.GetRegionWithOverride(model.Region)
networkAreaId := model.NetworkAreaId.ValueString()
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
// Generate API request body from model
@ -175,7 +354,7 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea
}
// Create new network area route
routes, err := r.client.CreateNetworkAreaRoute(ctx, organizationId, networkAreaId).CreateNetworkAreaRoutePayload(*payload).Execute()
routes, err := r.client.CreateNetworkAreaRoute(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRoutePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route", fmt.Sprintf("Calling API: %v", err))
return
@ -196,12 +375,12 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea
// Gets the route ID from the first element, routes.Items[0]
routeItems := *routes.Items
route := routeItems[0]
routeId := *route.RouteId
routeId := *route.Id
ctx = tflog.SetField(ctx, "network_area_route_id", routeId)
// Map response body to schema
err = mapFields(ctx, &route, &model)
err = mapFields(ctx, &route, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route.", fmt.Sprintf("Processing API payload: %v", err))
return
@ -217,7 +396,7 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea
// Read refreshes the Terraform state with the latest data.
func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
var model ModelV1
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -225,15 +404,17 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe
}
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
networkAreaRouteId := model.NetworkAreaRouteId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId)
networkAreaRouteResp, err := r.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).Execute()
networkAreaRouteResp, err := r.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -247,7 +428,7 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, networkAreaRouteResp, &model)
err = mapFields(ctx, networkAreaRouteResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err))
return
@ -264,7 +445,7 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe
// Delete deletes the resource and removes the Terraform state on success.
func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from state
var model Model
var model ModelV1
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -273,16 +454,18 @@ func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.Dele
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
networkAreaRouteId := model.NetworkAreaRouteId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId)
// Delete existing network
err := r.client.DeleteNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).Execute()
err := r.client.DeleteNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area route", fmt.Sprintf("Calling API: %v", err))
return
@ -296,7 +479,7 @@ func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.Dele
// Update updates the resource and sets the updated Terraform state on success.
func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
var model ModelV1
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -305,16 +488,18 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda
organizationId := model.OrganizationId.ValueString()
networkAreaId := model.NetworkAreaId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
networkAreaRouteId := model.NetworkAreaRouteId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId)
// Retrieve values from state
var stateModel Model
var stateModel ModelV1
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
@ -328,7 +513,7 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda
return
}
// Update existing network area route
networkAreaRouteResp, err := r.client.UpdateNetworkAreaRoute(ctx, organizationId, networkAreaId, networkAreaRouteId).UpdateNetworkAreaRoutePayload(*payload).Execute()
networkAreaRouteResp, err := r.client.UpdateNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).UpdateNetworkAreaRoutePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Calling API: %v", err))
return
@ -336,7 +521,7 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda
ctx = core.LogResponse(ctx)
err = mapFields(ctx, networkAreaRouteResp, &model)
err = mapFields(ctx, networkAreaRouteResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Processing API payload: %v", err))
return
@ -354,28 +539,25 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda
func (r *networkAreaRouteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing network area route",
fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[network_area_route_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[region],[network_area_route_id] Got: %q", req.ID),
)
return
}
organizationId := idParts[0]
networkAreaId := idParts[1]
networkAreaRouteId := idParts[2]
ctx = tflog.SetField(ctx, "organization_id", organizationId)
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"organization_id": idParts[0],
"network_area_id": idParts[1],
"region": idParts[2],
"network_area_route_id": idParts[3],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), organizationId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_id"), networkAreaId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_route_id"), networkAreaRouteId)...)
tflog.Info(ctx, "Network area route state imported")
}
func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model) error {
func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *ModelV1, region string) error {
if networkAreaRoute == nil {
return fmt.Errorf("response input is nil")
}
@ -386,13 +568,14 @@ func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model)
var networkAreaRouteId string
if model.NetworkAreaRouteId.ValueString() != "" {
networkAreaRouteId = model.NetworkAreaRouteId.ValueString()
} else if networkAreaRoute.RouteId != nil {
networkAreaRouteId = *networkAreaRoute.RouteId
} else if networkAreaRoute.Id != nil {
networkAreaRouteId = *networkAreaRoute.Id
} else {
return fmt.Errorf("network area route id not present")
}
model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), model.NetworkAreaId.ValueString(), networkAreaRouteId)
model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), model.NetworkAreaId.ValueString(), region, networkAreaRouteId)
model.Region = types.StringValue(region)
labels, err := iaasUtils.MapLabels(ctx, networkAreaRoute.Labels, model.Labels)
if err != nil {
@ -400,13 +583,22 @@ func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model)
}
model.NetworkAreaRouteId = types.StringValue(networkAreaRouteId)
model.NextHop = types.StringPointerValue(networkAreaRoute.Nexthop)
model.Prefix = types.StringPointerValue(networkAreaRoute.Prefix)
model.Labels = labels
model.NextHop, err = mapRouteNextHop(networkAreaRoute)
if err != nil {
return err
}
model.Destination, err = mapRouteDestination(networkAreaRoute)
if err != nil {
return err
}
return nil
}
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRoutePayload, error) {
func toCreatePayload(ctx context.Context, model *ModelV1) (*iaas.CreateNetworkAreaRoutePayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
@ -416,18 +608,28 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea
return nil, fmt.Errorf("converting to Go map: %w", err)
}
nextHopPayload, err := toNextHopPayload(model)
if err != nil {
return nil, err
}
destinationPayload, err := toDestinationPayload(model)
if err != nil {
return nil, err
}
return &iaas.CreateNetworkAreaRoutePayload{
Ipv4: &[]iaas.Route{
Items: &[]iaas.Route{
{
Prefix: conversion.StringValueToPointer(model.Prefix),
Nexthop: conversion.StringValueToPointer(model.NextHop),
Labels: &labels,
Destination: destinationPayload,
Labels: &labels,
Nexthop: nextHopPayload,
},
},
}, nil
}
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateNetworkAreaRoutePayload, error) {
func toUpdatePayload(ctx context.Context, model *ModelV1, currentLabels types.Map) (*iaas.UpdateNetworkAreaRoutePayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
@ -441,3 +643,97 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map)
Labels: &labels,
}, nil
}
func toNextHopPayload(model *ModelV1) (*iaas.RouteNexthop, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
} else if model.NextHop == nil {
return nil, fmt.Errorf("nexthop is nil in model")
}
switch model.NextHop.Type.ValueString() {
case "blackhole":
return sdkUtils.Ptr(iaas.NexthopBlackholeAsRouteNexthop(iaas.NewNexthopBlackhole("blackhole"))), nil
case "internet":
return sdkUtils.Ptr(iaas.NexthopInternetAsRouteNexthop(iaas.NewNexthopInternet("internet"))), nil
case "ipv4":
return sdkUtils.Ptr(iaas.NexthopIPv4AsRouteNexthop(iaas.NewNexthopIPv4("ipv4", model.NextHop.Value.ValueString()))), nil
case "ipv6":
return sdkUtils.Ptr(iaas.NexthopIPv6AsRouteNexthop(iaas.NewNexthopIPv6("ipv6", model.NextHop.Value.ValueString()))), nil
}
return nil, fmt.Errorf("unknown nexthop type: %s", model.NextHop.Type.ValueString())
}
func toDestinationPayload(model *ModelV1) (*iaas.RouteDestination, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
} else if model.Destination == nil {
return nil, fmt.Errorf("destination is nil in model")
}
switch model.Destination.Type.ValueString() {
case "cidrv4":
return sdkUtils.Ptr(iaas.DestinationCIDRv4AsRouteDestination(iaas.NewDestinationCIDRv4("cidrv4", model.Destination.Value.ValueString()))), nil
case "cidrv6":
return sdkUtils.Ptr(iaas.DestinationCIDRv6AsRouteDestination(iaas.NewDestinationCIDRv6("cidrv6", model.Destination.Value.ValueString()))), nil
}
return nil, fmt.Errorf("unknown destination type: %s", model.Destination.Type.ValueString())
}
func mapRouteNextHop(routeResp *iaas.Route) (*NexthopModelV1, error) {
if routeResp.Nexthop == nil {
return &NexthopModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
}, nil
}
switch i := routeResp.Nexthop.GetActualInstance().(type) {
case *iaas.NexthopIPv4:
return &NexthopModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringPointerValue(i.Value),
}, nil
case *iaas.NexthopIPv6:
return &NexthopModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringPointerValue(i.Value),
}, nil
case *iaas.NexthopBlackhole:
return &NexthopModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringNull(),
}, nil
case *iaas.NexthopInternet:
return &NexthopModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringNull(),
}, nil
default:
return nil, fmt.Errorf("unexpected nexthop type: %T", i)
}
}
func mapRouteDestination(routeResp *iaas.Route) (*DestinationModelV1, error) {
if routeResp.Destination == nil {
return &DestinationModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
}, nil
}
switch i := routeResp.Destination.GetActualInstance().(type) {
case *iaas.DestinationCIDRv4:
return &DestinationModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringPointerValue(i.Value),
}, nil
case *iaas.DestinationCIDRv6:
return &DestinationModelV1{
Type: types.StringPointerValue(i.Type),
Value: types.StringPointerValue(i.Value),
}, nil
default:
return nil, fmt.Errorf("unexpected Destionation type: %T", i)
}
}

View file

@ -2,100 +2,133 @@ package networkarearoute
import (
"context"
"reflect"
"testing"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
func TestMapFields(t *testing.T) {
type args struct {
state ModelV1
input *iaas.Route
region string
}
tests := []struct {
description string
state Model
input *iaas.Route
expected Model
args args
expected ModelV1
isValid bool
}{
{
"id_ok",
Model{
description: "id_ok",
args: args{
state: ModelV1{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
},
input: &iaas.Route{},
region: "eu01",
},
expected: ModelV1{
Id: types.StringValue("oid,naid,eu01,narid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
Destination: &DestinationModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
},
NextHop: &NexthopModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
},
Labels: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
&iaas.Route{},
Model{
Id: types.StringValue("oid,naid,narid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
Prefix: types.StringNull(),
NextHop: types.StringNull(),
Labels: types.MapNull(types.StringType),
},
true,
isValid: true,
},
{
"values_ok",
Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
},
&iaas.Route{
Prefix: utils.Ptr("prefix"),
Nexthop: utils.Ptr("hop"),
Labels: &map[string]interface{}{
"key": "value",
description: "values_ok",
args: args{
state: ModelV1{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
Region: types.StringValue("eu01"),
},
input: &iaas.Route{
Destination: &iaas.RouteDestination{
DestinationCIDRv4: &iaas.DestinationCIDRv4{
Type: utils.Ptr("cidrv4"),
Value: utils.Ptr("prefix"),
},
DestinationCIDRv6: nil,
},
Nexthop: &iaas.RouteNexthop{
NexthopIPv4: &iaas.NexthopIPv4{
Type: utils.Ptr("ipv4"),
Value: utils.Ptr("hop"),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
},
region: "eu02",
},
Model{
Id: types.StringValue("oid,naid,narid"),
expected: ModelV1{
Id: types.StringValue("oid,naid,eu02,narid"),
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
NetworkAreaRouteId: types.StringValue("narid"),
Prefix: types.StringValue("prefix"),
NextHop: types.StringValue("hop"),
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue("prefix"),
},
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue("hop"),
},
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"response_fields_nil_fail",
Model{},
&iaas.Route{
Prefix: nil,
Nexthop: nil,
description: "response_fields_nil_fail",
args: args{
input: &iaas.Route{
Destination: nil,
Nexthop: nil,
},
},
Model{},
false,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
Model{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
description: "no_resource_id",
args: args{
state: ModelV1{
OrganizationId: types.StringValue("oid"),
NetworkAreaId: types.StringValue("naid"),
},
input: &iaas.Route{},
},
&iaas.Route{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -103,7 +136,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
@ -115,24 +148,41 @@ func TestMapFields(t *testing.T) {
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
input *ModelV1
expected *iaas.CreateNetworkAreaRoutePayload
isValid bool
}{
{
description: "default_ok",
input: &Model{
Prefix: types.StringValue("prefix"),
NextHop: types.StringValue("hop"),
input: &ModelV1{
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue("prefix"),
},
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue("hop"),
},
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
},
expected: &iaas.CreateNetworkAreaRoutePayload{
Ipv4: &[]iaas.Route{
Items: &[]iaas.Route{
{
Prefix: utils.Ptr("prefix"),
Nexthop: utils.Ptr("hop"),
Destination: &iaas.RouteDestination{
DestinationCIDRv4: &iaas.DestinationCIDRv4{
Type: utils.Ptr("cidrv4"),
Value: utils.Ptr("prefix"),
},
DestinationCIDRv6: nil,
},
Nexthop: &iaas.RouteNexthop{
NexthopIPv4: &iaas.NexthopIPv4{
Type: utils.Ptr("ipv4"),
Value: utils.Ptr("hop"),
},
},
Labels: &map[string]interface{}{
"key": "value",
},
@ -164,13 +214,13 @@ func TestToCreatePayload(t *testing.T) {
func TestToUpdatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
input *ModelV1
expected *iaas.UpdateNetworkAreaRoutePayload
isValid bool
}{
{
"default_ok",
&Model{
&ModelV1{
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key1": types.StringValue("value1"),
"key2": types.StringValue("value2"),
@ -203,3 +253,371 @@ func TestToUpdatePayload(t *testing.T) {
})
}
}
func TestToNextHopPayload(t *testing.T) {
type args struct {
model *ModelV1
}
tests := []struct {
name string
args args
want *iaas.RouteNexthop
wantErr bool
}{
{
name: "ipv4",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue("10.20.30.40"),
},
},
},
want: &iaas.RouteNexthop{
NexthopIPv4: &iaas.NexthopIPv4{
Type: utils.Ptr("ipv4"),
Value: utils.Ptr("10.20.30.40"),
},
},
wantErr: false,
},
{
name: "ipv6",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("ipv6"),
Value: types.StringValue("2001:db8:85a3:0:0:8a2e:370:7334"),
},
},
},
want: &iaas.RouteNexthop{
NexthopIPv6: &iaas.NexthopIPv6{
Type: utils.Ptr("ipv6"),
Value: utils.Ptr("2001:db8:85a3:0:0:8a2e:370:7334"),
},
},
wantErr: false,
},
{
name: "internet",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("internet"),
},
},
},
want: &iaas.RouteNexthop{
NexthopInternet: &iaas.NexthopInternet{
Type: utils.Ptr("internet"),
},
},
wantErr: false,
},
{
name: "blackhole",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("blackhole"),
},
},
},
want: &iaas.RouteNexthop{
NexthopBlackhole: &iaas.NexthopBlackhole{
Type: utils.Ptr("blackhole"),
},
},
wantErr: false,
},
{
name: "invalid type",
args: args{
model: &ModelV1{
NextHop: &NexthopModelV1{
Type: types.StringValue("foobar"),
},
},
},
wantErr: true,
},
{
name: "model is nil",
args: args{
model: nil,
},
wantErr: true,
},
{
name: "nexthop in model is nil",
args: args{
model: &ModelV1{
NextHop: nil,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toNextHopPayload(tt.args.model)
if (err != nil) != tt.wantErr {
t.Errorf("toNextHopPayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("toNextHopPayload() got = %v, want %v", got, tt.want)
}
})
}
}
func TestToDestinationPayload(t *testing.T) {
type args struct {
model *ModelV1
}
tests := []struct {
name string
args args
want *iaas.RouteDestination
wantErr bool
}{
{
name: "cidrv4",
args: args{
model: &ModelV1{
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue("192.168.1.0/24"),
},
},
},
want: &iaas.RouteDestination{
DestinationCIDRv4: &iaas.DestinationCIDRv4{
Type: utils.Ptr("cidrv4"),
Value: utils.Ptr("192.168.1.0/24"),
},
},
wantErr: false,
},
{
name: "cidrv6",
args: args{
model: &ModelV1{
Destination: &DestinationModelV1{
Type: types.StringValue("cidrv6"),
Value: types.StringValue("2001:db8:1234::/48"),
},
},
},
want: &iaas.RouteDestination{
DestinationCIDRv6: &iaas.DestinationCIDRv6{
Type: utils.Ptr("cidrv6"),
Value: utils.Ptr("2001:db8:1234::/48"),
},
},
wantErr: false,
},
{
name: "invalid type",
args: args{
model: &ModelV1{
Destination: &DestinationModelV1{
Type: types.StringValue("foobar"),
},
},
},
wantErr: true,
},
{
name: "model is nil",
args: args{
model: nil,
},
wantErr: true,
},
{
name: "destination in model is nil",
args: args{
model: &ModelV1{
Destination: nil,
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toDestinationPayload(tt.args.model)
if (err != nil) != tt.wantErr {
t.Errorf("toDestinationPayload() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("toDestinationPayload() got = %v, want %v", got, tt.want)
}
})
}
}
func TestMapRouteNextHop(t *testing.T) {
type args struct {
routeResp *iaas.Route
}
tests := []struct {
name string
args args
want *NexthopModelV1
wantErr bool
}{
{
name: "ipv4",
args: args{
routeResp: &iaas.Route{
Nexthop: &iaas.RouteNexthop{
NexthopIPv4: &iaas.NexthopIPv4{
Type: utils.Ptr("ipv4"),
Value: utils.Ptr("192.168.1.0/24"),
},
},
},
},
want: &NexthopModelV1{
Type: types.StringValue("ipv4"),
Value: types.StringValue("192.168.1.0/24"),
},
},
{
name: "ipv6",
args: args{
routeResp: &iaas.Route{
Nexthop: &iaas.RouteNexthop{
NexthopIPv4: &iaas.NexthopIPv4{
Type: utils.Ptr("ipv6"),
Value: utils.Ptr("2001:db8:85a3:0:0:8a2e:370:7334"),
},
},
},
},
want: &NexthopModelV1{
Type: types.StringValue("ipv6"),
Value: types.StringValue("2001:db8:85a3:0:0:8a2e:370:7334"),
},
},
{
name: "blackhole",
args: args{
routeResp: &iaas.Route{
Nexthop: &iaas.RouteNexthop{
NexthopBlackhole: &iaas.NexthopBlackhole{
Type: utils.Ptr("blackhole"),
},
},
},
},
want: &NexthopModelV1{
Type: types.StringValue("blackhole"),
},
},
{
name: "internet",
args: args{
routeResp: &iaas.Route{
Nexthop: &iaas.RouteNexthop{
NexthopInternet: &iaas.NexthopInternet{
Type: utils.Ptr("internet"),
},
},
},
},
want: &NexthopModelV1{
Type: types.StringValue("internet"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mapRouteNextHop(tt.args.routeResp)
if (err != nil) != tt.wantErr {
t.Errorf("mapRouteNextHop() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mapRouteNextHop() got = %v, want %v", got, tt.want)
}
})
}
}
func TestMapRouteDestination(t *testing.T) {
type args struct {
routeResp *iaas.Route
}
tests := []struct {
name string
args args
want *DestinationModelV1
wantErr bool
}{
{
name: "cidrv4",
args: args{
routeResp: &iaas.Route{
Destination: &iaas.RouteDestination{
DestinationCIDRv4: &iaas.DestinationCIDRv4{
Type: utils.Ptr("cidrv4"),
Value: utils.Ptr("192.168.1.0/24"),
},
},
},
},
want: &DestinationModelV1{
Type: types.StringValue("cidrv4"),
Value: types.StringValue("192.168.1.0/24"),
},
},
{
name: "cidrv6",
args: args{
routeResp: &iaas.Route{
Destination: &iaas.RouteDestination{
DestinationCIDRv4: &iaas.DestinationCIDRv4{
Type: utils.Ptr("cidrv6"),
Value: utils.Ptr("2001:db8:1234::/48"),
},
},
},
},
want: &DestinationModelV1{
Type: types.StringValue("cidrv6"),
Value: types.StringValue("2001:db8:1234::/48"),
},
},
{
name: "destination in API response is nil",
args: args{
routeResp: &iaas.Route{
Destination: nil,
},
},
want: &DestinationModelV1{
Type: types.StringNull(),
Value: types.StringNull(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mapRouteDestination(tt.args.routeResp)
if (err != nil) != tt.wantErr {
t.Errorf("mapRouteDestination() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mapRouteDestination() got = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -24,14 +24,15 @@ var (
_ datasource.DataSource = &networkInterfaceDataSource{}
)
// NewNetworkDataSource is a helper function to simplify the provider implementation.
// NewNetworkInterfaceDataSource is a helper function to simplify the provider implementation.
func NewNetworkInterfaceDataSource() datasource.DataSource {
return &networkInterfaceDataSource{}
}
// networkInterfaceDataSource is the data source implementation.
type networkInterfaceDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -40,12 +41,13 @@ func (d *networkInterfaceDataSource) Metadata(_ context.Context, req datasource.
}
func (d *networkInterfaceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -63,7 +65,7 @@ func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.Sche
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal data source ID. It is structured as \"`project_id`,`network_id`,`network_interface_id`\".",
Description: "Terraform's internal data source ID. It is structured as \"`project_id`,`region`,`network_id`,`network_interface_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -74,6 +76,11 @@ func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.Sche
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"network_id": schema.StringAttribute{
Description: "The network ID to which the network interface is associated.",
Required: true,
@ -141,17 +148,20 @@ func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.Re
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
networkId := model.NetworkId.ValueString()
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
networkInterfaceResp, err := d.client.GetNic(ctx, projectId, networkId, networkInterfaceId).Execute()
networkInterfaceResp, err := d.client.GetNic(ctx, projectId, region, networkId, networkInterfaceId).Execute()
if err != nil {
utils.LogError(
ctx,
@ -169,7 +179,7 @@ func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.Re
ctx = core.LogResponse(ctx)
err = mapFields(ctx, networkInterfaceResp, &model)
err = mapFields(ctx, networkInterfaceResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Processing API payload: %v", err))
return

View file

@ -40,6 +40,7 @@ type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
NetworkId types.String `tfsdk:"network_id"`
Region types.String `tfsdk:"region"`
NetworkInterfaceId types.String `tfsdk:"network_interface_id"`
Name types.String `tfsdk:"name"`
AllowedAddresses types.List `tfsdk:"allowed_addresses"`
@ -59,7 +60,8 @@ func NewNetworkInterfaceResource() resource.Resource {
// networkResource is the resource implementation.
type networkInterfaceResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
@ -92,6 +94,17 @@ func (r *networkInterfaceResource) ModifyPlan(ctx context.Context, req resource.
if resp.Diagnostics.HasError() {
return
}
// Use the modifier to set the effective region in the current plan.
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Metadata returns the resource type name.
@ -101,12 +114,13 @@ func (r *networkInterfaceResource) Metadata(_ context.Context, req resource.Meta
// Configure adds the provider configured client to the resource.
func (r *networkInterfaceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -124,7 +138,7 @@ func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRe
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`,`network_interface_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`network_id`,`network_interface_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -164,6 +178,15 @@ func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRe
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"name": schema.StringAttribute{
Description: "The name of the network interface.",
Optional: true,
@ -260,8 +283,10 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
networkId := model.NetworkId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_id", networkId)
// Generate API request body from model
@ -272,7 +297,7 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea
}
// Create new network interface
networkInterface, err := r.client.CreateNic(ctx, projectId, networkId).CreateNicPayload(*payload).Execute()
networkInterface, err := r.client.CreateNic(ctx, projectId, region, networkId).CreateNicPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Calling API: %v", err))
return
@ -285,7 +310,7 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
// Map response body to schema
err = mapFields(ctx, networkInterface, &model)
err = mapFields(ctx, networkInterface, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Processing API payload: %v", err))
return
@ -308,16 +333,18 @@ func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRe
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
networkId := model.NetworkId.ValueString()
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
networkInterfaceResp, err := r.client.GetNic(ctx, projectId, networkId, networkInterfaceId).Execute()
networkInterfaceResp, err := r.client.GetNic(ctx, projectId, region, networkId, networkInterfaceId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -331,7 +358,7 @@ func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRe
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, networkInterfaceResp, &model)
err = mapFields(ctx, networkInterfaceResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Processing API payload: %v", err))
return
@ -355,12 +382,14 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
networkId := model.NetworkId.ValueString()
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
@ -379,7 +408,7 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda
return
}
// Update existing network
nicResp, err := r.client.UpdateNic(ctx, projectId, networkId, networkInterfaceId).UpdateNicPayload(*payload).Execute()
nicResp, err := r.client.UpdateNic(ctx, projectId, region, networkId, networkInterfaceId).UpdateNicPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Calling API: %v", err))
return
@ -387,7 +416,7 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda
ctx = core.LogResponse(ctx)
err = mapFields(ctx, nicResp, &model)
err = mapFields(ctx, nicResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Processing API payload: %v", err))
return
@ -411,17 +440,19 @@ func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.Dele
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
networkId := model.NetworkId.ValueString()
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
// Delete existing network interface
err := r.client.DeleteNic(ctx, projectId, networkId, networkInterfaceId).Execute()
err := r.client.DeleteNic(ctx, projectId, region, networkId, networkInterfaceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network interface", fmt.Sprintf("Calling API: %v", err))
return
@ -437,28 +468,25 @@ func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.Dele
func (r *networkInterfaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing network interface",
fmt.Sprintf("Expected import identifier with format: [project_id],[network_id],[network_interface_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id],[network_interface_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
networkId := idParts[1]
networkInterfaceId := idParts[2]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"network_id": idParts[2],
"network_interface_id": idParts[3],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_interface_id"), networkInterfaceId)...)
tflog.Info(ctx, "Network interface state imported")
}
func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model) error {
func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model, region string) error {
if networkInterfaceResp == nil {
return fmt.Errorf("response input is nil")
}
@ -475,7 +503,8 @@ func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model
return fmt.Errorf("network interface id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.NetworkId.ValueString(), networkInterfaceId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.NetworkId.ValueString(), networkInterfaceId)
model.Region = types.StringValue(region)
respAllowedAddresses := []string{}
var diags diag.Diagnostics

View file

@ -12,25 +12,32 @@ import (
)
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.NIC
region string
}
tests := []struct {
description string
state Model
input *iaas.NIC
args args
expected Model
isValid bool
}{
{
"id_ok",
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
description: "id_ok",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
},
input: &iaas.NIC{
Id: utils.Ptr("nicid"),
},
region: "eu01",
},
&iaas.NIC{
Id: utils.Ptr("nicid"),
},
Model{
Id: types.StringValue("pid,nid,nicid"),
expected: Model{
Id: types.StringValue("pid,eu01,nid,nicid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
@ -43,41 +50,46 @@ func TestMapFields(t *testing.T) {
Mac: types.StringNull(),
Type: types.StringNull(),
Labels: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"values_ok",
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
},
&iaas.NIC{
Id: utils.Ptr("nicid"),
Name: utils.Ptr("name"),
AllowedAddresses: &[]iaas.AllowedAddressesInner{
{
String: utils.Ptr("aa1"),
description: "values_ok",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
Region: types.StringValue("eu01"),
},
input: &iaas.NIC{
Id: utils.Ptr("nicid"),
Name: utils.Ptr("name"),
AllowedAddresses: &[]iaas.AllowedAddressesInner{
{
String: utils.Ptr("aa1"),
},
},
SecurityGroups: &[]string{
"prefix1",
"prefix2",
},
Ipv4: utils.Ptr("ipv4"),
Ipv6: utils.Ptr("ipv6"),
NicSecurity: utils.Ptr(true),
Device: utils.Ptr("device"),
Mac: utils.Ptr("mac"),
Status: utils.Ptr("status"),
Type: utils.Ptr("type"),
Labels: &map[string]interface{}{
"label1": "ref1",
},
},
SecurityGroups: &[]string{
"prefix1",
"prefix2",
},
Ipv4: utils.Ptr("ipv4"),
Ipv6: utils.Ptr("ipv6"),
NicSecurity: utils.Ptr(true),
Device: utils.Ptr("device"),
Mac: utils.Ptr("mac"),
Status: utils.Ptr("status"),
Type: utils.Ptr("type"),
Labels: &map[string]interface{}{
"label1": "ref1",
},
region: "eu02",
},
Model{
Id: types.StringValue("pid,nid,nicid"),
expected: Model{
Id: types.StringValue("pid,eu02,nid,nicid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
@ -95,29 +107,33 @@ func TestMapFields(t *testing.T) {
Mac: types.StringValue("mac"),
Type: types.StringValue("type"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"label1": types.StringValue("ref1")}),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"allowed_addresses_changed_outside_tf",
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("aa1"),
}),
},
&iaas.NIC{
Id: utils.Ptr("nicid"),
AllowedAddresses: &[]iaas.AllowedAddressesInner{
{
String: utils.Ptr("aa2"),
description: "allowed_addresses_changed_outside_tf",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("aa1"),
}),
},
input: &iaas.NIC{
Id: utils.Ptr("nicid"),
AllowedAddresses: &[]iaas.AllowedAddressesInner{
{
String: utils.Ptr("aa2"),
},
},
},
region: "eu01",
},
Model{
Id: types.StringValue("pid,nid,nicid"),
expected: Model{
Id: types.StringValue("pid,eu01,nid,nicid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
@ -127,23 +143,27 @@ func TestMapFields(t *testing.T) {
types.StringValue("aa2"),
}),
Labels: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"empty_list_allowed_addresses",
Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{}),
description: "empty_list_allowed_addresses",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{}),
},
input: &iaas.NIC{
Id: utils.Ptr("nicid"),
AllowedAddresses: nil,
},
region: "eu01",
},
&iaas.NIC{
Id: utils.Ptr("nicid"),
AllowedAddresses: nil,
},
Model{
Id: types.StringValue("pid,nid,nicid"),
expected: Model{
Id: types.StringValue("pid,eu01,nid,nicid"),
ProjectId: types.StringValue("pid"),
NetworkId: types.StringValue("nid"),
NetworkInterfaceId: types.StringValue("nicid"),
@ -151,29 +171,34 @@ func TestMapFields(t *testing.T) {
SecurityGroupIds: types.ListNull(types.StringType),
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{}),
Labels: types.MapNull(types.StringType),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
args: args{
state: Model{},
input: nil,
},
expected: Model{},
isValid: false,
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.NIC{},
},
&iaas.NIC{},
Model{},
false,
expected: Model{},
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -181,7 +206,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -11,7 +11,6 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@ -30,11 +29,13 @@ var (
_ resource.Resource = &networkInterfaceAttachResource{}
_ resource.ResourceWithConfigure = &networkInterfaceAttachResource{}
_ resource.ResourceWithImportState = &networkInterfaceAttachResource{}
_ resource.ResourceWithModifyPlan = &networkInterfaceAttachResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
ServerId types.String `tfsdk:"server_id"`
NetworkInterfaceId types.String `tfsdk:"network_interface_id"`
}
@ -46,7 +47,8 @@ func NewNetworkInterfaceAttachResource() resource.Resource {
// networkInterfaceAttachResource is the resource implementation.
type networkInterfaceAttachResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -54,14 +56,45 @@ func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resourc
resp.TypeName = req.ProviderTypeName + "_server_network_interface_attach"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *networkInterfaceAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -71,13 +104,13 @@ func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req reso
// Schema defines the schema for the resource.
func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration. The attachment only takes full effect after server reboot."
description := "Network interface attachment resource schema. Attaches a network interface to a server. The attachment only takes full effect after server reboot."
resp.Schema = schema.Schema{
MarkdownDescription: description,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`network_interface_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`network_interface_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -94,6 +127,15 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"server_id": schema.StringAttribute{
Description: "The server ID.",
Required: true,
@ -133,14 +175,16 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
// Create new network interface attachment
err := r.client.AddNicToServer(ctx, projectId, serverId, networkInterfaceId).Execute()
err := r.client.AddNicToServer(ctx, projectId, region, serverId, networkInterfaceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching network interface to server", fmt.Sprintf("Calling API: %v", err))
return
@ -148,7 +192,8 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc
ctx = core.LogResponse(ctx)
model.Id = utils.BuildInternalTerraformId(projectId, serverId, networkInterfaceId)
model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, networkInterfaceId)
model.Region = types.StringValue(region)
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
@ -171,13 +216,14 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
nics, err := r.client.ListServerNics(ctx, projectId, serverId).Execute()
nics, err := r.client.ListServerNICs(ctx, projectId, region, serverId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -200,12 +246,17 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.
if nic.Id == nil || (nic.Id != nil && *nic.Id != networkInterfaceId) {
continue
}
model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, networkInterfaceId)
model.Region = types.StringValue(region)
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Network interface attachment read")
return
}
@ -233,14 +284,16 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
network_interfaceId := model.NetworkInterfaceId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "network_interface_id", network_interfaceId)
// Remove network_interface from server
err := r.client.RemoveNicFromServer(ctx, projectId, serverId, network_interfaceId).Execute()
err := r.client.RemoveNicFromServer(ctx, projectId, region, serverId, network_interfaceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing network interface from server", fmt.Sprintf("Calling API: %v", err))
return
@ -256,23 +309,20 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc
func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing network_interface attachment",
fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[network_interface_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[network_interface_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
serverId := idParts[1]
network_interfaceId := idParts[2]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "network_interface_id", network_interfaceId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{
"project_id": idParts[0],
"region": idParts[1],
"server_id": idParts[2],
"network_interface_id": idParts[3],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_interface_id"), network_interfaceId)...)
tflog.Info(ctx, "Network interface attachment state imported")
}

View file

@ -28,9 +28,12 @@ type DatasourceModel struct {
ProjectId types.String `tfsdk:"project_id"`
AreaId types.String `tfsdk:"area_id"`
InternetAccess types.Bool `tfsdk:"internet_access"`
State types.String `tfsdk:"state"`
Status types.String `tfsdk:"status"`
CreatedAt types.String `tfsdk:"created_at"`
UpdatedAt types.String `tfsdk:"updated_at"`
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
State types.String `tfsdk:"state"`
}
// NewProjectDataSource is a helper function to simplify the provider implementation.
@ -70,7 +73,7 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
"project_id": "STACKIT project ID.",
"area_id": "The area ID to which the project belongs to.",
"internet_access": "Specifies if the project has internet_access",
"state": "Specifies the state of the project.",
"status": "Specifies the status of the project.",
"created_at": "Date-time when the project was created.",
"updated_at": "Date-time when the project was last updated.",
}
@ -98,8 +101,14 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
Description: descriptions["internet_access"],
Computed: true,
},
// Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider.
"state": schema.StringAttribute{
Description: descriptions["state"],
DeprecationMessage: "Deprecated: Will be removed in May 2026. Use the `status` field instead.",
Description: descriptions["status"],
Computed: true,
},
"status": schema.StringAttribute{
Description: descriptions["status"],
Computed: true,
},
"created_at": schema.StringAttribute{
@ -170,8 +179,8 @@ func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) erro
var projectId string
if model.ProjectId.ValueString() != "" {
projectId = model.ProjectId.ValueString()
} else if projectResp.ProjectId != nil {
projectId = *projectResp.ProjectId
} else if projectResp.Id != nil {
projectId = *projectResp.Id
} else {
return fmt.Errorf("project id is not present")
}
@ -202,7 +211,8 @@ func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) erro
model.AreaId = areaId
model.InternetAccess = types.BoolPointerValue(projectResp.InternetAccess)
model.State = types.StringPointerValue(projectResp.State)
model.State = types.StringPointerValue(projectResp.Status)
model.Status = types.StringPointerValue(projectResp.Status)
model.CreatedAt = createdAt
model.UpdatedAt = updatedAt
return nil

View file

@ -34,7 +34,7 @@ func TestMapDataSourceFields(t *testing.T) {
ProjectId: types.StringValue(projectId),
},
input: &iaas.Project{
ProjectId: utils.Ptr(projectId),
Id: utils.Ptr(projectId),
},
expected: &DatasourceModel{
Id: types.StringValue(projectId),
@ -48,13 +48,12 @@ func TestMapDataSourceFields(t *testing.T) {
ProjectId: types.StringValue(projectId),
},
input: &iaas.Project{
AreaId: utils.Ptr(iaas.AreaId{String: utils.Ptr("aid")}),
CreatedAt: utils.Ptr(testTimestamp()),
InternetAccess: utils.Ptr(true),
OpenstackProjectId: utils.Ptr("oid"),
ProjectId: utils.Ptr(projectId),
State: utils.Ptr("CREATED"),
UpdatedAt: utils.Ptr(testTimestamp()),
AreaId: utils.Ptr(iaas.AreaId{String: utils.Ptr("aid")}),
CreatedAt: utils.Ptr(testTimestamp()),
InternetAccess: utils.Ptr(true),
Id: utils.Ptr(projectId),
Status: utils.Ptr("CREATED"),
UpdatedAt: utils.Ptr(testTimestamp()),
},
expected: &DatasourceModel{
Id: types.StringValue(projectId),
@ -62,6 +61,7 @@ func TestMapDataSourceFields(t *testing.T) {
AreaId: types.StringValue("aid"),
InternetAccess: types.BoolValue(true),
State: types.StringValue("CREATED"),
Status: types.StringValue("CREATED"),
CreatedAt: types.StringValue(testTimestampValue),
UpdatedAt: types.StringValue(testTimestampValue),
},
@ -76,7 +76,7 @@ func TestMapDataSourceFields(t *testing.T) {
AreaId: utils.Ptr(iaas.AreaId{
StaticAreaID: iaas.STATICAREAID_PUBLIC.Ptr(),
}),
ProjectId: utils.Ptr(projectId),
Id: utils.Ptr(projectId),
},
expected: &DatasourceModel{
Id: types.StringValue(projectId),

View file

@ -24,14 +24,15 @@ var (
_ datasource.DataSource = &publicIpDataSource{}
)
// NewVolumeDataSource is a helper function to simplify the provider implementation.
// NewPublicIpDataSource is a helper function to simplify the provider implementation.
func NewPublicIpDataSource() datasource.DataSource {
return &publicIpDataSource{}
}
// publicIpDataSource is the data source implementation.
type publicIpDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -40,12 +41,13 @@ func (d *publicIpDataSource) Metadata(_ context.Context, req datasource.Metadata
}
func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -54,14 +56,14 @@ func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.Confi
}
// Schema defines the schema for the resource.
func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (d *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "Public IP resource schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: description,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`public_ip_id`\".",
Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`region`,`public_ip_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -72,6 +74,11 @@ func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaReques
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"public_ip_id": schema.StringAttribute{
Description: "The public IP ID.",
Required: true,
@ -110,14 +117,16 @@ func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadReques
return
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
publicIpId := model.PublicIpId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
publicIpResp, err := d.client.GetPublicIP(ctx, projectId, publicIpId).Execute()
publicIpResp, err := d.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute()
if err != nil {
utils.LogError(
ctx,
@ -135,7 +144,7 @@ func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadReques
ctx = core.LogResponse(ctx)
err = mapFields(ctx, publicIpResp, &model)
err = mapFields(ctx, publicIpResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Processing API payload: %v", err))
return

View file

@ -10,7 +10,6 @@ import (
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@ -30,11 +29,13 @@ var (
_ resource.Resource = &publicIpResource{}
_ resource.ResourceWithConfigure = &publicIpResource{}
_ resource.ResourceWithImportState = &publicIpResource{}
_ resource.ResourceWithModifyPlan = &publicIpResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
PublicIpId types.String `tfsdk:"public_ip_id"`
Ip types.String `tfsdk:"ip"`
NetworkInterfaceId types.String `tfsdk:"network_interface_id"`
@ -48,7 +49,8 @@ func NewPublicIpResource() resource.Resource {
// publicIpResource is the resource implementation.
type publicIpResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -56,14 +58,45 @@ func (r *publicIpResource) Metadata(_ context.Context, req resource.MetadataRequ
resp.TypeName = req.ProviderTypeName + "_public_ip"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *publicIpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *publicIpResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -79,7 +112,7 @@ func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, r
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`public_ip_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`public_ip_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -96,6 +129,15 @@ func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, r
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"public_ip_id": schema.StringAttribute{
Description: "The public IP ID.",
Computed: true,
@ -148,7 +190,9 @@ func (r *publicIpResource) Create(ctx context.Context, req resource.CreateReques
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
@ -159,7 +203,7 @@ func (r *publicIpResource) Create(ctx context.Context, req resource.CreateReques
// Create new public IP
publicIp, err := r.client.CreatePublicIP(ctx, projectId).CreatePublicIPPayload(*payload).Execute()
publicIp, err := r.client.CreatePublicIP(ctx, projectId, region).CreatePublicIPPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Calling API: %v", err))
return
@ -170,7 +214,7 @@ func (r *publicIpResource) Create(ctx context.Context, req resource.CreateReques
ctx = tflog.SetField(ctx, "public_ip_id", *publicIp.Id)
// Map response body to schema
err = mapFields(ctx, publicIp, &model)
err = mapFields(ctx, publicIp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Processing API payload: %v", err))
return
@ -193,14 +237,16 @@ func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, r
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
publicIpId := model.PublicIpId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
publicIpResp, err := r.client.GetPublicIP(ctx, projectId, publicIpId).Execute()
publicIpResp, err := r.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -214,7 +260,7 @@ func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, r
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, publicIpResp, &model)
err = mapFields(ctx, publicIpResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Processing API payload: %v", err))
return
@ -238,11 +284,13 @@ func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateReques
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
publicIpId := model.PublicIpId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
// Retrieve values from state
@ -260,7 +308,7 @@ func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateReques
return
}
// Update existing public IP
updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, publicIpId).UpdatePublicIPPayload(*payload).Execute()
updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Calling API: %v", err))
return
@ -268,7 +316,7 @@ func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateReques
ctx = core.LogResponse(ctx)
err = mapFields(ctx, updatedPublicIp, &model)
err = mapFields(ctx, updatedPublicIp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Processing API payload: %v", err))
return
@ -292,15 +340,17 @@ func (r *publicIpResource) Delete(ctx context.Context, req resource.DeleteReques
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
publicIpId := model.PublicIpId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
// Delete existing publicIp
err := r.client.DeletePublicIP(ctx, projectId, publicIpId).Execute()
err := r.client.DeletePublicIP(ctx, projectId, region, publicIpId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting public IP", fmt.Sprintf("Calling API: %v", err))
return
@ -316,25 +366,24 @@ func (r *publicIpResource) Delete(ctx context.Context, req resource.DeleteReques
func (r *publicIpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing public IP",
fmt.Sprintf("Expected import identifier with format: [project_id],[public_ip_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[public_ip_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
publicIpId := idParts[1]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"public_ip_id": idParts[2],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("public_ip_id"), publicIpId)...)
tflog.Info(ctx, "public IP state imported")
}
func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model) error {
func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model, region string) error {
if publicIpResp == nil {
return fmt.Errorf("response input is nil")
}
@ -351,7 +400,8 @@ func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model) e
return fmt.Errorf("public IP id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), publicIpId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, publicIpId)
model.Region = types.StringValue(region)
labels, err := iaasUtils.MapLabels(ctx, publicIpResp.Labels, model.Labels)
if err != nil {

View file

@ -12,49 +12,61 @@ import (
)
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.PublicIp
region string
}
tests := []struct {
description string
state Model
input *iaas.PublicIp
args args
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
},
input: &iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(nil),
},
region: "eu01",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(nil),
},
Model{
Id: types.StringValue("pid,pipid"),
expected: Model{
Id: types.StringValue("pid,eu01,pipid"),
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Ip: types.StringNull(),
Labels: types.MapNull(types.StringType),
NetworkInterfaceId: types.StringNull(),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
Ip: utils.Ptr("ip"),
Labels: &map[string]interface{}{
"key": "value",
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Region: types.StringValue("eu01"),
},
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
input: &iaas.PublicIp{
Id: utils.Ptr("pipid"),
Ip: utils.Ptr("ip"),
Labels: &map[string]interface{}{
"key": "value",
},
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
},
region: "eu02",
},
Model{
Id: types.StringValue("pid,pipid"),
expected: Model{
Id: types.StringValue("pid,eu02,pipid"),
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Ip: types.StringValue("ip"),
@ -62,69 +74,74 @@ func TestMapFields(t *testing.T) {
"key": types.StringValue("value"),
}),
NetworkInterfaceId: types.StringValue("interface"),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"empty_labels",
Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
description: "empty_labels",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
input: &iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
},
region: "eu01",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
},
Model{
Id: types.StringValue("pid,pipid"),
expected: Model{
Id: types.StringValue("pid,eu01,pipid"),
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Ip: types.StringNull(),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
NetworkInterfaceId: types.StringValue("interface"),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"network_interface_id_nil",
Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
description: "network_interface_id_nil",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
},
input: &iaas.PublicIp{
Id: utils.Ptr("pipid"),
},
region: "eu01",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
},
Model{
Id: types.StringValue("pid,pipid"),
expected: Model{
Id: types.StringValue("pid,eu01,pipid"),
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Ip: types.StringNull(),
Labels: types.MapNull(types.StringType),
NetworkInterfaceId: types.StringNull(),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.PublicIp{},
},
&iaas.PublicIp{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -132,7 +149,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -10,7 +10,6 @@ import (
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@ -30,11 +29,13 @@ var (
_ resource.Resource = &publicIpAssociateResource{}
_ resource.ResourceWithConfigure = &publicIpAssociateResource{}
_ resource.ResourceWithImportState = &publicIpAssociateResource{}
_ resource.ResourceWithModifyPlan = &publicIpAssociateResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
PublicIpId types.String `tfsdk:"public_ip_id"`
Ip types.String `tfsdk:"ip"`
NetworkInterfaceId types.String `tfsdk:"network_interface_id"`
@ -47,7 +48,8 @@ func NewPublicIpAssociateResource() resource.Resource {
// publicIpAssociateResource is the resource implementation.
type publicIpAssociateResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -55,14 +57,45 @@ func (r *publicIpAssociateResource) Metadata(_ context.Context, req resource.Met
resp.TypeName = req.ProviderTypeName + "_public_ip_associate"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *publicIpAssociateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *publicIpAssociateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -88,7 +121,7 @@ func (r *publicIpAssociateResource) Schema(_ context.Context, _ resource.SchemaR
Description: fmt.Sprintf("%s\n\n%s", descriptions["main"], descriptions["warning_message"]),
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`public_ip_id`,`network_interface_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`public_ip_id`,`network_interface_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -105,6 +138,15 @@ func (r *publicIpAssociateResource) Schema(_ context.Context, _ resource.SchemaR
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"public_ip_id": schema.StringAttribute{
Description: "The public IP ID.",
Required: true,
@ -151,12 +193,14 @@ func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.Cre
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
publicIpId := model.PublicIpId.ValueString()
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
@ -167,7 +211,7 @@ func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.Cre
return
}
// Update existing public IP
updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, publicIpId).UpdatePublicIPPayload(*payload).Execute()
updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error associating public IP to network interface", fmt.Sprintf("Calling API: %v", err))
return
@ -175,7 +219,7 @@ func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.Cre
ctx = core.LogResponse(ctx)
err = mapFields(updatedPublicIp, &model)
err = mapFields(updatedPublicIp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error associating public IP to network interface", fmt.Sprintf("Processing API payload: %v", err))
return
@ -197,16 +241,18 @@ func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadR
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
publicIpId := model.PublicIpId.ValueString()
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
publicIpResp, err := r.client.GetPublicIP(ctx, projectId, publicIpId).Execute()
publicIpResp, err := r.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -220,7 +266,7 @@ func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadR
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(publicIpResp, &model)
err = mapFields(publicIpResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP association", fmt.Sprintf("Processing API payload: %v", err))
return
@ -250,12 +296,14 @@ func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.Del
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
publicIpId := model.PublicIpId.ValueString()
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
@ -263,7 +311,7 @@ func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.Del
NetworkInterface: iaas.NewNullableString(nil),
}
_, err := r.client.UpdatePublicIP(ctx, projectId, publicIpId).UpdatePublicIPPayload(*payload).Execute()
_, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting public IP association", fmt.Sprintf("Calling API: %v", err))
return
@ -279,28 +327,25 @@ func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.Del
func (r *publicIpAssociateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing public IP associate",
fmt.Sprintf("Expected import identifier with format: [project_id],[public_ip_id],[network_interface_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[public_ip_id],[network_interface_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
publicIpId := idParts[1]
networkInterfaceId := idParts[2]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"public_ip_id": idParts[2],
"network_interface_id": idParts[3],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("public_ip_id"), publicIpId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_interface_id"), networkInterfaceId)...)
tflog.Info(ctx, "public IP state imported")
}
func mapFields(publicIpResp *iaas.PublicIp, model *Model) error {
func mapFields(publicIpResp *iaas.PublicIp, model *Model, region string) error {
if publicIpResp == nil {
return fmt.Errorf("response input is nil")
}
@ -324,8 +369,9 @@ func mapFields(publicIpResp *iaas.PublicIp, model *Model) error {
}
model.Id = utils.BuildInternalTerraformId(
model.ProjectId.ValueString(), publicIpId, model.NetworkInterfaceId.ValueString(),
model.ProjectId.ValueString(), region, publicIpId, model.NetworkInterfaceId.ValueString(),
)
model.Region = types.StringValue(region)
model.PublicIpId = types.StringValue(publicIpId)
model.Ip = types.StringPointerValue(publicIpResp.Ip)

View file

@ -10,74 +10,82 @@ import (
)
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.PublicIp
region string
}
tests := []struct {
description string
state Model
input *iaas.PublicIp
args args
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
NetworkInterfaceId: types.StringValue("nicid"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
NetworkInterfaceId: types.StringValue("nicid"),
},
input: &iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")),
},
region: "eu01",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")),
},
Model{
Id: types.StringValue("pid,pipid,nicid"),
expected: Model{
Id: types.StringValue("pid,eu01,pipid,nicid"),
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Ip: types.StringNull(),
NetworkInterfaceId: types.StringValue("nicid"),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
NetworkInterfaceId: types.StringValue("nicid"),
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
NetworkInterfaceId: types.StringValue("nicid"),
},
input: &iaas.PublicIp{
Id: utils.Ptr("pipid"),
Ip: utils.Ptr("ip"),
NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")),
},
region: "eu02",
},
&iaas.PublicIp{
Id: utils.Ptr("pipid"),
Ip: utils.Ptr("ip"),
NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")),
},
Model{
Id: types.StringValue("pid,pipid,nicid"),
expected: Model{
Id: types.StringValue("pid,eu02,pipid,nicid"),
ProjectId: types.StringValue("pid"),
PublicIpId: types.StringValue("pipid"),
Ip: types.StringValue("ip"),
NetworkInterfaceId: types.StringValue("nicid"),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.PublicIp{},
},
&iaas.PublicIp{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(tt.input, &tt.state)
err := mapFields(tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -85,7 +93,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -31,7 +31,8 @@ func NewSecurityGroupDataSource() datasource.DataSource {
// securityGroupDataSource is the data source implementation.
type securityGroupDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -40,12 +41,13 @@ func (d *securityGroupDataSource) Metadata(_ context.Context, req datasource.Met
}
func (d *securityGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -54,14 +56,14 @@ func (d *securityGroupDataSource) Configure(ctx context.Context, req datasource.
}
// Schema defines the schema for the resource.
func (r *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (d *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "Security group datasource schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: description,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -72,6 +74,11 @@ func (r *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaR
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"security_group_id": schema.StringAttribute{
Description: "The security group ID.",
Required: true,
@ -110,14 +117,16 @@ func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadR
return
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
securityGroupId := model.SecurityGroupId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
securityGroupResp, err := d.client.GetSecurityGroup(ctx, projectId, securityGroupId).Execute()
securityGroupResp, err := d.client.GetSecurityGroup(ctx, projectId, region, securityGroupId).Execute()
if err != nil {
utils.LogError(
ctx,
@ -135,7 +144,7 @@ func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadR
ctx = core.LogResponse(ctx)
err = mapFields(ctx, securityGroupResp, &model)
err = mapFields(ctx, securityGroupResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Processing API payload: %v", err))
return

View file

@ -12,7 +12,6 @@ import (
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
@ -33,11 +32,13 @@ var (
_ resource.Resource = &securityGroupResource{}
_ resource.ResourceWithConfigure = &securityGroupResource{}
_ resource.ResourceWithImportState = &securityGroupResource{}
_ resource.ResourceWithModifyPlan = &securityGroupResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
SecurityGroupId types.String `tfsdk:"security_group_id"`
Name types.String `tfsdk:"name"`
Description types.String `tfsdk:"description"`
@ -52,7 +53,8 @@ func NewSecurityGroupResource() resource.Resource {
// securityGroupResource is the resource implementation.
type securityGroupResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -60,14 +62,45 @@ func (r *securityGroupResource) Metadata(_ context.Context, req resource.Metadat
resp.TypeName = req.ProviderTypeName + "_security_group"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *securityGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *securityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -83,7 +116,7 @@ func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaReque
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -100,6 +133,15 @@ func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaReque
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"security_group_id": schema.StringAttribute{
Description: "The security group ID.",
Computed: true,
@ -165,7 +207,9 @@ func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateR
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
@ -176,7 +220,7 @@ func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateR
// Create new security group
securityGroup, err := r.client.CreateSecurityGroup(ctx, projectId).CreateSecurityGroupPayload(*payload).Execute()
securityGroup, err := r.client.CreateSecurityGroup(ctx, projectId, region).CreateSecurityGroupPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Calling API: %v", err))
return
@ -189,7 +233,7 @@ func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateR
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
// Map response body to schema
err = mapFields(ctx, securityGroup, &model)
err = mapFields(ctx, securityGroup, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Processing API payload: %v", err))
return
@ -212,14 +256,16 @@ func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadReque
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
securityGroupId := model.SecurityGroupId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "security_id", securityGroupId)
securityGroupResp, err := r.client.GetSecurityGroup(ctx, projectId, securityGroupId).Execute()
securityGroupResp, err := r.client.GetSecurityGroup(ctx, projectId, region, securityGroupId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -233,7 +279,7 @@ func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadReque
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, securityGroupResp, &model)
err = mapFields(ctx, securityGroupResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Processing API payload: %v", err))
return
@ -257,11 +303,13 @@ func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateR
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
securityGroupId := model.SecurityGroupId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
// Retrieve values from state
@ -279,7 +327,7 @@ func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateR
return
}
// Update existing security group
updatedSecurityGroup, err := r.client.UpdateSecurityGroup(ctx, projectId, securityGroupId).UpdateSecurityGroupPayload(*payload).Execute()
updatedSecurityGroup, err := r.client.UpdateSecurityGroup(ctx, projectId, region, securityGroupId).UpdateSecurityGroupPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Calling API: %v", err))
return
@ -287,7 +335,7 @@ func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateR
ctx = core.LogResponse(ctx)
err = mapFields(ctx, updatedSecurityGroup, &model)
err = mapFields(ctx, updatedSecurityGroup, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Processing API payload: %v", err))
return
@ -311,15 +359,17 @@ func (r *securityGroupResource) Delete(ctx context.Context, req resource.DeleteR
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
securityGroupId := model.SecurityGroupId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
// Delete existing security group
err := r.client.DeleteSecurityGroup(ctx, projectId, securityGroupId).Execute()
err := r.client.DeleteSecurityGroup(ctx, projectId, region, securityGroupId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group", fmt.Sprintf("Calling API: %v", err))
return
@ -335,25 +385,24 @@ func (r *securityGroupResource) Delete(ctx context.Context, req resource.DeleteR
func (r *securityGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing security group",
fmt.Sprintf("Expected import identifier with format: [project_id],[security_group_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[security_group_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
securityGroupId := idParts[1]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"security_group_id": idParts[2],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_id"), securityGroupId)...)
tflog.Info(ctx, "security group state imported")
}
func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model *Model) error {
func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model *Model, region string) error {
if securityGroupResp == nil {
return fmt.Errorf("response input is nil")
}
@ -370,7 +419,8 @@ func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model
return fmt.Errorf("security group id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), securityGroupId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, securityGroupId)
model.Region = types.StringValue(region)
labels, err := iaasUtils.MapLabels(ctx, securityGroupResp.Labels, model.Labels)
if err != nil {

View file

@ -12,51 +12,62 @@ import (
)
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.SecurityGroup
region string
}
tests := []struct {
description string
state Model
input *iaas.SecurityGroup
args args
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
},
input: &iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
},
region: "eu01",
},
&iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
},
Model{
Id: types.StringValue("pid,sgid"),
expected: Model{
Id: types.StringValue("pid,eu01,sgid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
Name: types.StringNull(),
Labels: types.MapNull(types.StringType),
Description: types.StringNull(),
Stateful: types.BoolNull(),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
},
// &sourceModel{},
&iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
Name: utils.Ptr("name"),
Stateful: utils.Ptr(true),
Labels: &map[string]interface{}{
"key": "value",
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
Region: types.StringValue("eu01"),
},
Description: utils.Ptr("desc"),
input: &iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
Name: utils.Ptr("name"),
Stateful: utils.Ptr(true),
Labels: &map[string]interface{}{
"key": "value",
},
Description: utils.Ptr("desc"),
},
region: "eu02",
},
Model{
Id: types.StringValue("pid,sgid"),
expected: Model{
Id: types.StringValue("pid,eu02,sgid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
Name: types.StringValue("name"),
@ -65,50 +76,51 @@ func TestMapFields(t *testing.T) {
}),
Description: types.StringValue("desc"),
Stateful: types.BoolValue(true),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"empty_labels",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
description: "empty_labels",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
},
input: &iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
Labels: &map[string]interface{}{},
},
region: "eu01",
},
&iaas.SecurityGroup{
Id: utils.Ptr("sgid"),
Labels: &map[string]interface{}{},
},
Model{
Id: types.StringValue("pid,sgid"),
expected: Model{
Id: types.StringValue("pid,eu01,sgid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
Name: types.StringNull(),
Labels: types.MapNull(types.StringType),
Description: types.StringNull(),
Stateful: types.BoolNull(),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.SecurityGroup{},
},
&iaas.SecurityGroup{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -116,7 +128,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -30,7 +30,8 @@ func NewSecurityGroupRuleDataSource() datasource.DataSource {
// securityGroupRuleDataSource is the data source implementation.
type securityGroupRuleDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -39,12 +40,13 @@ func (d *securityGroupRuleDataSource) Metadata(_ context.Context, req datasource
}
func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -53,7 +55,7 @@ func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasou
}
// Schema defines the schema for the resource.
func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (d *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
directionOptions := []string{"ingress", "egress"}
description := "Security group datasource schema. Must have a `region` specified in the provider configuration."
@ -62,7 +64,7 @@ func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.Sch
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`security_group_id`,`security_group_rule_id`\".",
Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`region`,`security_group_id`,`security_group_rule_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -89,6 +91,11 @@ func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.Sch
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"direction": schema.StringAttribute{
Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.FormatPossibleValues(directionOptions...),
Computed: true,
@ -164,16 +171,18 @@ func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.R
return
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
securityGroupId := model.SecurityGroupId.ValueString()
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId)
securityGroupRuleResp, err := d.client.GetSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute()
securityGroupRuleResp, err := d.client.GetSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute()
if err != nil {
utils.LogError(
ctx,
@ -191,7 +200,7 @@ func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.R
ctx = core.LogResponse(ctx)
err = mapFields(securityGroupRuleResp, &model)
err = mapFields(securityGroupRuleResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Processing API payload: %v", err))
return

View file

@ -34,11 +34,13 @@ import (
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &securityGroupRuleResource{}
_ resource.ResourceWithConfigure = &securityGroupRuleResource{}
_ resource.ResourceWithImportState = &securityGroupRuleResource{}
icmpProtocols = []string{"icmp", "ipv6-icmp"}
protocolsPossibleValues = []string{
_ resource.Resource = &securityGroupRuleResource{}
_ resource.ResourceWithConfigure = &securityGroupRuleResource{}
_ resource.ResourceWithImportState = &securityGroupRuleResource{}
_ resource.ResourceWithModifyPlan = &securityGroupRuleResource{}
icmpProtocols = []string{"icmp", "ipv6-icmp"}
protocolsPossibleValues = []string{
"ah", "dccp", "egp", "esp", "gre", "icmp", "igmp", "ipip", "ipv6-encap", "ipv6-frag", "ipv6-icmp",
"ipv6-nonxt", "ipv6-opts", "ipv6-route", "ospf", "pgm", "rsvp", "sctp", "tcp", "udp", "udplite", "vrrp",
}
@ -47,6 +49,7 @@ var (
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
SecurityGroupId types.String `tfsdk:"security_group_id"`
SecurityGroupRuleId types.String `tfsdk:"security_group_rule_id"`
Direction types.String `tfsdk:"direction"`
@ -99,7 +102,8 @@ func NewSecurityGroupRuleResource() resource.Resource {
// securityGroupRuleResource is the resource implementation.
type securityGroupRuleResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -107,14 +111,45 @@ func (r *securityGroupRuleResource) Metadata(_ context.Context, req resource.Met
resp.TypeName = req.ProviderTypeName + "_security_group_rule"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *securityGroupRuleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -122,7 +157,7 @@ func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource.
tflog.Info(ctx, "iaas client configured")
}
func (r securityGroupRuleResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
func (r *securityGroupRuleResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var model Model
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
@ -178,7 +213,7 @@ func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaR
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`,`security_group_rule_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`,`security_group_rule_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -196,6 +231,15 @@ func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaR
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"security_group_id": schema.StringAttribute{
Description: "The security group ID.",
Required: true,
@ -392,8 +436,10 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
securityGroupId := model.SecurityGroupId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
var icmpParameters *icmpParametersModel
@ -434,7 +480,7 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre
}
// Create new security group rule
securityGroupRule, err := r.client.CreateSecurityGroupRule(ctx, projectId, securityGroupId).CreateSecurityGroupRulePayload(*payload).Execute()
securityGroupRule, err := r.client.CreateSecurityGroupRule(ctx, projectId, region, securityGroupId).CreateSecurityGroupRulePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Calling API: %v", err))
return
@ -445,7 +491,7 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre
ctx = tflog.SetField(ctx, "security_group_rule_id", *securityGroupRule.Id)
// Map response body to schema
err = mapFields(securityGroupRule, &model)
err = mapFields(securityGroupRule, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Processing API payload: %v", err))
return
@ -468,16 +514,18 @@ func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadR
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
securityGroupId := model.SecurityGroupId.ValueString()
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId)
securityGroupRuleResp, err := r.client.GetSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute()
securityGroupRuleResp, err := r.client.GetSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -491,7 +539,7 @@ func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadR
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(securityGroupRuleResp, &model)
err = mapFields(securityGroupRuleResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Processing API payload: %v", err))
return
@ -522,17 +570,19 @@ func (r *securityGroupRuleResource) Delete(ctx context.Context, req resource.Del
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
securityGroupId := model.SecurityGroupId.ValueString()
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId)
// Delete existing security group rule
err := r.client.DeleteSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute()
err := r.client.DeleteSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group rule", fmt.Sprintf("Calling API: %v", err))
return
@ -548,28 +598,25 @@ func (r *securityGroupRuleResource) Delete(ctx context.Context, req resource.Del
func (r *securityGroupRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing security group rule",
fmt.Sprintf("Expected import identifier with format: [project_id],[security_group_id],[security_group_rule_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[security_group_id],[security_group_rule_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
securityGroupId := idParts[1]
securityGroupRuleId := idParts[2]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"security_group_id": idParts[2],
"security_group_rule_id": idParts[3],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_id"), securityGroupId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_rule_id"), securityGroupRuleId)...)
tflog.Info(ctx, "security group rule state imported")
}
func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model) error {
func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model, region string) error {
if securityGroupRuleResp == nil {
return fmt.Errorf("response input is nil")
}
@ -586,7 +633,8 @@ func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model) erro
return fmt.Errorf("security group rule id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.SecurityGroupId.ValueString(), securityGroupRuleId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.SecurityGroupId.ValueString(), securityGroupRuleId)
model.Region = types.StringValue(region)
model.SecurityGroupRuleId = types.StringValue(securityGroupRuleId)
model.Direction = types.StringPointerValue(securityGroupRuleResp.Direction)
model.Description = types.StringPointerValue(securityGroupRuleResp.Description)

View file

@ -52,25 +52,32 @@ var fixtureCreateProtocol = iaas.CreateProtocol{
}
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.SecurityGroupRule
region string
}
tests := []struct {
description string
state Model
input *iaas.SecurityGroupRule
args args
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
},
input: &iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
},
region: "eu01",
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
},
Model{
Id: types.StringValue("pid,sgid,sgrid"),
expected: Model{
Id: types.StringValue("pid,eu01,sgid,sgrid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
@ -82,29 +89,34 @@ func TestMapFields(t *testing.T) {
IcmpParameters: types.ObjectNull(icmpParametersTypes),
PortRange: types.ObjectNull(portRangeTypes),
Protocol: types.ObjectNull(protocolTypes),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Region: types.StringValue("eu01"),
},
input: &iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Description: utils.Ptr("desc"),
Direction: utils.Ptr("ingress"),
Ethertype: utils.Ptr("ether"),
IpRange: utils.Ptr("iprange"),
RemoteSecurityGroupId: utils.Ptr("remote"),
IcmpParameters: &fixtureIcmpParameters,
PortRange: &fixturePortRange,
Protocol: &fixtureProtocol,
},
region: "eu02",
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Description: utils.Ptr("desc"),
Direction: utils.Ptr("ingress"),
Ethertype: utils.Ptr("ether"),
IpRange: utils.Ptr("iprange"),
RemoteSecurityGroupId: utils.Ptr("remote"),
IcmpParameters: &fixtureIcmpParameters,
PortRange: &fixturePortRange,
Protocol: &fixtureProtocol,
},
Model{
Id: types.StringValue("pid,sgid,sgrid"),
expected: Model{
Id: types.StringValue("pid,eu02,sgid,sgrid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
@ -116,26 +128,30 @@ func TestMapFields(t *testing.T) {
IcmpParameters: fixtureModelIcmpParameters,
PortRange: fixtureModelPortRange,
Protocol: fixtureModelProtocol,
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"protocol_only_with_name",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{
"name": types.StringValue("name"),
"number": types.Int64Null(),
}),
description: "protocol_only_with_name",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{
"name": types.StringValue("name"),
"number": types.Int64Null(),
}),
},
input: &iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Protocol: &fixtureProtocol,
},
region: "eu01",
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Protocol: &fixtureProtocol,
},
Model{
Id: types.StringValue("pid,sgid,sgrid"),
expected: Model{
Id: types.StringValue("pid,eu01,sgid,sgrid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
@ -147,26 +163,30 @@ func TestMapFields(t *testing.T) {
IcmpParameters: types.ObjectNull(icmpParametersTypes),
PortRange: types.ObjectNull(portRangeTypes),
Protocol: fixtureModelProtocol,
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"protocol_only_with_number",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{
"name": types.StringNull(),
"number": types.Int64Value(1),
}),
description: "protocol_only_with_number",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{
"name": types.StringNull(),
"number": types.Int64Value(1),
}),
},
input: &iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Protocol: &fixtureProtocol,
},
region: "eu01",
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Protocol: &fixtureProtocol,
},
Model{
Id: types.StringValue("pid,sgid,sgrid"),
expected: Model{
Id: types.StringValue("pid,eu01,sgid,sgrid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
@ -178,30 +198,27 @@ func TestMapFields(t *testing.T) {
IcmpParameters: types.ObjectNull(icmpParametersTypes),
PortRange: types.ObjectNull(portRangeTypes),
Protocol: fixtureModelProtocol,
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
},
input: &iaas.SecurityGroupRule{},
},
&iaas.SecurityGroupRule{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(tt.input, &tt.state)
err := mapFields(tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -209,7 +226,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -30,6 +30,7 @@ var (
type DataSourceModel struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
ServerId types.String `tfsdk:"server_id"`
MachineType types.String `tfsdk:"machine_type"`
Name types.String `tfsdk:"name"`
@ -58,7 +59,8 @@ func NewServerDataSource() datasource.DataSource {
// serverDataSource is the data source implementation.
type serverDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -67,12 +69,13 @@ func (d *serverDataSource) Metadata(_ context.Context, req datasource.MetadataRe
}
func (d *serverDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -81,14 +84,14 @@ func (d *serverDataSource) Configure(ctx context.Context, req datasource.Configu
}
// Schema defines the schema for the datasource.
func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (d *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "Server datasource schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: description,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -99,6 +102,11 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"server_id": schema.StringAttribute{
Description: "The server ID.",
Required: true,
@ -175,8 +183,8 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
}
}
// // Read refreshes the Terraform state with the latest data.
func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
// Read refreshes the Terraform state with the latest data.
func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model DataSourceModel
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -184,14 +192,16 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
return
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
serverReq := r.client.GetServer(ctx, projectId, serverId)
serverReq := d.client.GetServer(ctx, projectId, region, serverId)
serverReq = serverReq.Details(true)
serverResp, err := serverReq.Execute()
if err != nil {
@ -212,7 +222,7 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapDataSourceFields(ctx, serverResp, &model)
err = mapDataSourceFields(ctx, serverResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Processing API payload: %v", err))
return
@ -226,7 +236,7 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
tflog.Info(ctx, "server read")
}
func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *DataSourceModel) error {
func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *DataSourceModel, region string) error {
if serverResp == nil {
return fmt.Errorf("response input is nil")
}
@ -243,7 +253,8 @@ func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *Da
return fmt.Errorf("server id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), serverId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, serverId)
model.Region = types.StringValue(region)
labels, err := iaasUtils.MapLabels(ctx, serverResp.Labels, model.Labels)
if err != nil {

View file

@ -12,24 +12,31 @@ import (
)
func TestMapDataSourceFields(t *testing.T) {
type args struct {
state DataSourceModel
input *iaas.Server
region string
}
tests := []struct {
description string
state DataSourceModel
input *iaas.Server
args args
expected DataSourceModel
isValid bool
}{
{
"default_values",
DataSourceModel{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
description: "default_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
},
input: &iaas.Server{
Id: utils.Ptr("sid"),
},
region: "eu01",
},
&iaas.Server{
Id: utils.Ptr("sid"),
},
DataSourceModel{
Id: types.StringValue("pid,sid"),
expected: DataSourceModel{
Id: types.StringValue("pid,eu01,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
@ -43,40 +50,45 @@ func TestMapDataSourceFields(t *testing.T) {
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
DataSourceModel{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
},
&iaas.Server{
Id: utils.Ptr("sid"),
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
description: "simple_values",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Region: types.StringValue("eu01"),
},
ImageId: utils.Ptr("image_id"),
Nics: &[]iaas.ServerNetwork{
{
NicId: utils.Ptr("nic1"),
input: &iaas.Server{
Id: utils.Ptr("sid"),
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
},
{
NicId: utils.Ptr("nic2"),
ImageId: utils.Ptr("image_id"),
Nics: &[]iaas.ServerNetwork{
{
NicId: utils.Ptr("nic1"),
},
{
NicId: utils.Ptr("nic2"),
},
},
KeypairName: utils.Ptr("keypair_name"),
AffinityGroup: utils.Ptr("group_id"),
CreatedAt: utils.Ptr(testTimestamp()),
UpdatedAt: utils.Ptr(testTimestamp()),
LaunchedAt: utils.Ptr(testTimestamp()),
Status: utils.Ptr("active"),
},
KeypairName: utils.Ptr("keypair_name"),
AffinityGroup: utils.Ptr("group_id"),
CreatedAt: utils.Ptr(testTimestamp()),
UpdatedAt: utils.Ptr(testTimestamp()),
LaunchedAt: utils.Ptr(testTimestamp()),
Status: utils.Ptr("active"),
region: "eu02",
},
DataSourceModel{
Id: types.StringValue("pid,sid"),
expected: DataSourceModel{
Id: types.StringValue("pid,eu02,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringValue("name"),
@ -94,21 +106,25 @@ func TestMapDataSourceFields(t *testing.T) {
CreatedAt: types.StringValue(testTimestampValue),
UpdatedAt: types.StringValue(testTimestampValue),
LaunchedAt: types.StringValue(testTimestampValue),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"empty_labels",
DataSourceModel{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
description: "empty_labels",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
input: &iaas.Server{
Id: utils.Ptr("sid"),
},
region: "eu01",
},
&iaas.Server{
Id: utils.Ptr("sid"),
},
DataSourceModel{
Id: types.StringValue("pid,sid"),
expected: DataSourceModel{
Id: types.StringValue("pid,eu01,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
@ -122,29 +138,26 @@ func TestMapDataSourceFields(t *testing.T) {
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"response_nil_fail",
DataSourceModel{},
nil,
DataSourceModel{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
DataSourceModel{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: DataSourceModel{
ProjectId: types.StringValue("pid"),
},
input: &iaas.Server{},
},
&iaas.Server{},
DataSourceModel{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapDataSourceFields(context.Background(), tt.input, &tt.state)
err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -152,7 +165,7 @@ func TestMapDataSourceFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -42,6 +42,7 @@ var (
_ resource.Resource = &serverResource{}
_ resource.ResourceWithConfigure = &serverResource{}
_ resource.ResourceWithImportState = &serverResource{}
_ resource.ResourceWithModifyPlan = &serverResource{}
supportedSourceTypes = []string{"volume", "image"}
desiredStatusOptions = []string{modelStateActive, modelStateInactive, modelStateDeallocated}
@ -56,6 +57,7 @@ const (
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
ServerId types.String `tfsdk:"server_id"`
MachineType types.String `tfsdk:"machine_type"`
Name types.String `tfsdk:"name"`
@ -100,7 +102,8 @@ func NewServerResource() resource.Resource {
// serverResource is the resource implementation.
type serverResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -108,7 +111,37 @@ func (r *serverResource) Metadata(_ context.Context, req resource.MetadataReques
resp.TypeName = req.ProviderTypeName + "_server"
}
func (r serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var model Model
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
@ -129,6 +162,10 @@ func (r serverResource) ValidateConfig(ctx context.Context, req resource.Validat
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring server", "You can only provide `delete_on_termination` for `source_type` `image`.")
}
}
if model.NetworkInterfaces.IsNull() || model.NetworkInterfaces.IsUnknown() || len(model.NetworkInterfaces.Elements()) < 1 {
core.LogAndAddWarning(ctx, &resp.Diagnostics, "No network interfaces configured", "You have no network interfaces configured for this server. This will be a problem when you want to (re-)create this server. Please note that modifying the network interfaces for an existing server will result in a replacement of the resource. We will provide a clear migration path soon.")
}
}
// ConfigValidators validates the resource configuration
@ -147,12 +184,13 @@ func (r *serverResource) ConfigValidators(_ context.Context) []resource.ConfigVa
// Configure adds the provider configured client to the resource.
func (r *serverResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -167,7 +205,7 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res
Description: "Server resource schema. Must have a `region` specified in the provider configuration.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -184,6 +222,15 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"server_id": schema.StringAttribute{
Description: "The server ID.",
Computed: true,
@ -297,7 +344,7 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res
},
},
"network_interfaces": schema.ListAttribute{
Description: "The IDs of network interfaces which should be attached to the server. Updating it will recreate the server.",
Description: "The IDs of network interfaces which should be attached to the server. Updating it will recreate the server. **Required when (re-)creating servers. Still marked as optional in the schema to not introduce breaking changes. There will be a migration path for this field soon.**",
Optional: true,
ElementType: types.StringType,
Validators: []validator.List{
@ -428,11 +475,12 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
@ -442,7 +490,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
// Create new server
server, err := r.client.CreateServer(ctx, projectId).CreateServerPayload(*payload).Execute()
server, err := r.client.CreateServer(ctx, projectId, region).CreateServerPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Calling API: %v", err))
return
@ -451,7 +499,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
ctx = core.LogResponse(ctx)
serverId := *server.Id
_, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
_, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err))
return
@ -459,7 +507,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
ctx = tflog.SetField(ctx, "server_id", serverId)
// Get Server with details
serverReq := r.client.GetServer(ctx, projectId, serverId)
serverReq := r.client.GetServer(ctx, projectId, region, serverId)
serverReq = serverReq.Details(true)
server, err = serverReq.Execute()
if err != nil {
@ -467,14 +515,14 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
}
// Map response body to schema
err = mapFields(ctx, server, &model)
err = mapFields(ctx, server, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Processing API payload: %v", err))
return
}
if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creting server", fmt.Sprintf("update server state: %v", err))
if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("update server state: %v", err))
return
}
@ -491,41 +539,41 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
// client operations in [updateServerStatus]
type serverControlClient interface {
wait.APIClientInterface
StartServerExecute(ctx context.Context, projectId string, serverId string) error
StopServerExecute(ctx context.Context, projectId string, serverId string) error
DeallocateServerExecute(ctx context.Context, projectId string, serverId string) error
StartServerExecute(ctx context.Context, projectId string, region string, serverId string) error
StopServerExecute(ctx context.Context, projectId string, region string, serverId string) error
DeallocateServerExecute(ctx context.Context, projectId string, region string, serverId string) error
}
func startServer(ctx context.Context, client serverControlClient, projectId, serverId string) error {
func startServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error {
tflog.Debug(ctx, "starting server to enter active state")
if err := client.StartServerExecute(ctx, projectId, serverId); err != nil {
if err := client.StartServerExecute(ctx, projectId, region, serverId); err != nil {
return fmt.Errorf("cannot start server: %w", err)
}
_, err := wait.StartServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx)
_, err := wait.StartServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("cannot check started server: %w", err)
}
return nil
}
func stopServer(ctx context.Context, client serverControlClient, projectId, serverId string) error {
func stopServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error {
tflog.Debug(ctx, "stopping server to enter inactive state")
if err := client.StopServerExecute(ctx, projectId, serverId); err != nil {
if err := client.StopServerExecute(ctx, projectId, region, serverId); err != nil {
return fmt.Errorf("cannot stop server: %w", err)
}
_, err := wait.StopServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx)
_, err := wait.StopServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("cannot check stopped server: %w", err)
}
return nil
}
func deallocatServer(ctx context.Context, client serverControlClient, projectId, serverId string) error {
func deallocateServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error {
tflog.Debug(ctx, "deallocating server to enter shelved state")
if err := client.DeallocateServerExecute(ctx, projectId, serverId); err != nil {
if err := client.DeallocateServerExecute(ctx, projectId, region, serverId); err != nil {
return fmt.Errorf("cannot deallocate server: %w", err)
}
_, err := wait.DeallocateServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx)
_, err := wait.DeallocateServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx)
if err != nil {
return fmt.Errorf("cannot check deallocated server: %w", err)
}
@ -533,7 +581,7 @@ func deallocatServer(ctx context.Context, client serverControlClient, projectId,
}
// updateServerStatus applies the appropriate server state changes for the actual current and the intended state
func updateServerStatus(ctx context.Context, client serverControlClient, currentState *string, model *Model) error {
func updateServerStatus(ctx context.Context, client serverControlClient, currentState *string, model *Model, region string) error {
if currentState == nil {
tflog.Warn(ctx, "no current state available, not updating server state")
return nil
@ -542,52 +590,52 @@ func updateServerStatus(ctx context.Context, client serverControlClient, current
case wait.ServerActiveStatus:
switch strings.ToUpper(model.DesiredStatus.ValueString()) {
case wait.ServerInactiveStatus:
if err := stopServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if err := stopServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
case wait.ServerDeallocatedStatus:
if err := deallocatServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if err := deallocateServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
default:
tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString()))
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
}
case wait.ServerInactiveStatus:
switch strings.ToUpper(model.DesiredStatus.ValueString()) {
case wait.ServerActiveStatus:
if err := startServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if err := startServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
case wait.ServerDeallocatedStatus:
if err := deallocatServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if err := deallocateServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
default:
tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString()))
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
}
case wait.ServerDeallocatedStatus:
switch strings.ToUpper(model.DesiredStatus.ValueString()) {
case wait.ServerActiveStatus:
if err := startServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if err := startServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
case wait.ServerInactiveStatus:
if err := stopServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if err := stopServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
default:
tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString()))
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil {
if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil {
return err
}
}
@ -598,7 +646,7 @@ func updateServerStatus(ctx context.Context, client serverControlClient, current
return nil
}
// // Read refreshes the Terraform state with the latest data.
// Read refreshes the Terraform state with the latest data.
func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
@ -607,14 +655,16 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
serverReq := r.client.GetServer(ctx, projectId, serverId)
serverReq := r.client.GetServer(ctx, projectId, region, serverId)
serverReq = serverReq.Details(true)
serverResp, err := serverReq.Execute()
if err != nil {
@ -630,7 +680,7 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, serverResp, &model)
err = mapFields(ctx, serverResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Processing API payload: %v", err))
return
@ -644,7 +694,7 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
tflog.Info(ctx, "server read")
}
func (r *serverResource) updateServerAttributes(ctx context.Context, model, stateModel *Model) (*iaas.Server, error) {
func (r *serverResource) updateServerAttributes(ctx context.Context, model, stateModel *Model, region string) (*iaas.Server, error) {
// Generate API request body from model
payload, err := toUpdatePayload(ctx, model, stateModel.Labels)
if err != nil {
@ -655,7 +705,7 @@ func (r *serverResource) updateServerAttributes(ctx context.Context, model, stat
var updatedServer *iaas.Server
// Update existing server
updatedServer, err = r.client.UpdateServer(ctx, projectId, serverId).UpdateServerPayload(*payload).Execute()
updatedServer, err = r.client.UpdateServer(ctx, projectId, region, serverId).UpdateServerPayload(*payload).Execute()
if err != nil {
return nil, fmt.Errorf("Calling API: %w", err)
}
@ -666,12 +716,12 @@ func (r *serverResource) updateServerAttributes(ctx context.Context, model, stat
payload := iaas.ResizeServerPayload{
MachineType: modelMachineType,
}
err := r.client.ResizeServer(ctx, projectId, serverId).ResizeServerPayload(payload).Execute()
err := r.client.ResizeServer(ctx, projectId, region, serverId).ResizeServerPayload(payload).Execute()
if err != nil {
return nil, fmt.Errorf("Resizing the server, calling API: %w", err)
}
_, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
_, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx)
if err != nil {
return nil, fmt.Errorf("server resize waiting: %w", err)
}
@ -691,11 +741,13 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
// Retrieve values from state
@ -710,14 +762,14 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
server *iaas.Server
err error
)
if server, err = r.client.GetServer(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()).Execute(); err != nil {
if server, err = r.client.GetServer(ctx, projectId, region, serverId).Execute(); err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error retrieving server state", fmt.Sprintf("Getting server state: %v", err))
}
if model.DesiredStatus.ValueString() == modelStateDeallocated {
// if the target state is "deallocated", we have to perform the server update first
// and then shelve it afterwards. A shelved server cannot be updated
_, err = r.updateServerAttributes(ctx, &model, &stateModel)
_, err = r.updateServerAttributes(ctx, &model, &stateModel, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error())
return
@ -725,18 +777,18 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
ctx = core.LogResponse(ctx)
if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil {
if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error())
return
}
} else {
// potentially unfreeze first and update afterwards
if err := updateServerStatus(ctx, r.client, server.Status, &model); err != nil {
if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error())
return
}
_, err = r.updateServerAttributes(ctx, &model, &stateModel)
_, err = r.updateServerAttributes(ctx, &model, &stateModel, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error())
return
@ -746,7 +798,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
}
// Re-fetch the server data, to get the details values.
serverReq := r.client.GetServer(ctx, projectId, serverId)
serverReq := r.client.GetServer(ctx, projectId, region, serverId)
serverReq = serverReq.Details(true)
updatedServer, err := serverReq.Execute()
if err != nil {
@ -754,7 +806,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest,
return
}
err = mapFields(ctx, updatedServer, &model)
err = mapFields(ctx, updatedServer, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Processing API payload: %v", err))
return
@ -779,15 +831,17 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
// Delete existing server
err := r.client.DeleteServer(ctx, projectId, serverId).Execute()
err := r.client.DeleteServer(ctx, projectId, region, serverId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("Calling API: %v", err))
return
@ -795,7 +849,7 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest,
ctx = core.LogResponse(ctx)
_, err = wait.DeleteServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
_, err = wait.DeleteServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("server deletion waiting: %v", err))
return
@ -809,25 +863,24 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest,
func (r *serverResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing server",
fmt.Sprintf("Expected import identifier with format: [project_id],[server_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
serverId := idParts[1]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"server_id": idParts[2],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
tflog.Info(ctx, "server state imported")
}
func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error {
func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model, region string) error {
if serverResp == nil {
return fmt.Errorf("response input is nil")
}
@ -844,7 +897,8 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error
return fmt.Errorf("server id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), serverId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, serverId)
model.Region = types.StringValue(region)
labels, err := iaasUtils.MapLabels(ctx, serverResp.Labels, model.Labels)
if err != nil {
@ -981,9 +1035,9 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo
return nil, fmt.Errorf("converting to Go map: %w", err)
}
var bootVolumePayload *iaas.CreateServerPayloadBootVolume
var bootVolumePayload *iaas.ServerBootVolume
if !bootVolume.SourceId.IsNull() && !bootVolume.SourceType.IsNull() {
bootVolumePayload = &iaas.CreateServerPayloadBootVolume{
bootVolumePayload = &iaas.ServerBootVolume{
PerformanceClass: conversion.StringValueToPointer(bootVolume.PerformanceClass),
Size: conversion.Int64ValueToPointer(bootVolume.Size),
Source: &iaas.BootVolumeSource{
@ -1005,22 +1059,22 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo
userData = &encodedUserData
}
var network *iaas.CreateServerPayloadNetworking
if !model.NetworkInterfaces.IsNull() && !model.NetworkInterfaces.IsUnknown() {
var nicIds []string
for _, nic := range model.NetworkInterfaces.Elements() {
nicString, ok := nic.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
nicIds = append(nicIds, nicString.ValueString())
if model.NetworkInterfaces.IsNull() || model.NetworkInterfaces.IsUnknown() {
return nil, fmt.Errorf("nil network interfaces")
}
var nicIds []string
for _, nic := range model.NetworkInterfaces.Elements() {
nicString, ok := nic.(types.String)
if !ok {
return nil, fmt.Errorf("type assertion failed")
}
nicIds = append(nicIds, nicString.ValueString())
}
network = &iaas.CreateServerPayloadNetworking{
CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{
NicIds: &nicIds,
},
}
network := &iaas.CreateServerPayloadAllOfNetworking{
CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{
NicIds: &nicIds,
},
}
return &iaas.CreateServerPayload{

View file

@ -26,24 +26,31 @@ func testTimestamp() time.Time {
}
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.Server
region string
}
tests := []struct {
description string
state Model
input *iaas.Server
args args
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
},
input: &iaas.Server{
Id: utils.Ptr("sid"),
},
region: "eu01",
},
&iaas.Server{
Id: utils.Ptr("sid"),
},
Model{
Id: types.StringValue("pid,sid"),
expected: Model{
Id: types.StringValue("pid,eu01,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
@ -57,40 +64,45 @@ func TestMapFields(t *testing.T) {
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
},
&iaas.Server{
Id: utils.Ptr("sid"),
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Region: types.StringValue("eu01"),
},
ImageId: utils.Ptr("image_id"),
Nics: &[]iaas.ServerNetwork{
{
NicId: utils.Ptr("nic1"),
input: &iaas.Server{
Id: utils.Ptr("sid"),
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
},
{
NicId: utils.Ptr("nic2"),
ImageId: utils.Ptr("image_id"),
Nics: &[]iaas.ServerNetwork{
{
NicId: utils.Ptr("nic1"),
},
{
NicId: utils.Ptr("nic2"),
},
},
KeypairName: utils.Ptr("keypair_name"),
AffinityGroup: utils.Ptr("group_id"),
CreatedAt: utils.Ptr(testTimestamp()),
UpdatedAt: utils.Ptr(testTimestamp()),
LaunchedAt: utils.Ptr(testTimestamp()),
Status: utils.Ptr("active"),
},
KeypairName: utils.Ptr("keypair_name"),
AffinityGroup: utils.Ptr("group_id"),
CreatedAt: utils.Ptr(testTimestamp()),
UpdatedAt: utils.Ptr(testTimestamp()),
LaunchedAt: utils.Ptr(testTimestamp()),
Status: utils.Ptr("active"),
region: "eu02",
},
Model{
Id: types.StringValue("pid,sid"),
expected: Model{
Id: types.StringValue("pid,eu02,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringValue("name"),
@ -105,21 +117,25 @@ func TestMapFields(t *testing.T) {
CreatedAt: types.StringValue(testTimestampValue),
UpdatedAt: types.StringValue(testTimestampValue),
LaunchedAt: types.StringValue(testTimestampValue),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"empty_labels",
Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
description: "empty_labels",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
input: &iaas.Server{
Id: utils.Ptr("sid"),
},
region: "eu01",
},
&iaas.Server{
Id: utils.Ptr("sid"),
},
Model{
Id: types.StringValue("pid,sid"),
expected: Model{
Id: types.StringValue("pid,eu01,sid"),
ProjectId: types.StringValue("pid"),
ServerId: types.StringValue("sid"),
Name: types.StringNull(),
@ -133,29 +149,26 @@ func TestMapFields(t *testing.T) {
CreatedAt: types.StringNull(),
UpdatedAt: types.StringNull(),
LaunchedAt: types.StringNull(),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.Server{},
},
&iaas.Server{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -163,7 +176,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
@ -180,8 +193,8 @@ func TestToCreatePayload(t *testing.T) {
isValid bool
}{
{
"ok",
&Model{
description: "ok",
input: &Model{
Name: types.StringValue("name"),
AvailabilityZone: types.StringValue("zone"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
@ -199,14 +212,18 @@ func TestToCreatePayload(t *testing.T) {
KeypairName: types.StringValue("keypair"),
MachineType: types.StringValue("machine_type"),
UserData: types.StringValue(userData),
NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("nic1"),
types.StringValue("nic2"),
}),
},
&iaas.CreateServerPayload{
expected: &iaas.CreateServerPayload{
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
},
BootVolume: &iaas.CreateServerPayloadBootVolume{
BootVolume: &iaas.ServerBootVolume{
PerformanceClass: utils.Ptr("class"),
Size: utils.Ptr(int64(1)),
Source: &iaas.BootVolumeSource{
@ -218,12 +235,17 @@ func TestToCreatePayload(t *testing.T) {
KeypairName: utils.Ptr("keypair"),
MachineType: utils.Ptr("machine_type"),
UserData: utils.Ptr([]byte(base64EncodedUserData)),
Networking: &iaas.CreateServerPayloadAllOfNetworking{
CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{
NicIds: &[]string{"nic1", "nic2"},
},
},
},
true,
isValid: true,
},
{
"delete on termination is set to true",
&Model{
description: "delete on termination is set to true",
input: &Model{
Name: types.StringValue("name"),
AvailabilityZone: types.StringValue("zone"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
@ -241,14 +263,18 @@ func TestToCreatePayload(t *testing.T) {
KeypairName: types.StringValue("keypair"),
MachineType: types.StringValue("machine_type"),
UserData: types.StringValue(userData),
NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("nic1"),
types.StringValue("nic2"),
}),
},
&iaas.CreateServerPayload{
expected: &iaas.CreateServerPayload{
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
},
BootVolume: &iaas.CreateServerPayloadBootVolume{
BootVolume: &iaas.ServerBootVolume{
PerformanceClass: utils.Ptr("class"),
Size: utils.Ptr(int64(1)),
Source: &iaas.BootVolumeSource{
@ -261,8 +287,13 @@ func TestToCreatePayload(t *testing.T) {
KeypairName: utils.Ptr("keypair"),
MachineType: utils.Ptr("machine_type"),
UserData: utils.Ptr([]byte(base64EncodedUserData)),
Networking: &iaas.CreateServerPayloadAllOfNetworking{
CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{
NicIds: &[]string{"nic1", "nic2"},
},
},
},
true,
isValid: true,
},
}
for _, tt := range tests {
@ -327,47 +358,47 @@ func TestToUpdatePayload(t *testing.T) {
}
}
var _ serverControlClient = (*mockServerControlClient)(nil)
var _ serverControlClient = &mockServerControlClient{}
// mockServerControlClient mocks the [serverControlClient] interface with
// pluggable functions
type mockServerControlClient struct {
wait.APIClientInterface
startServerCalled int
startServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error
startServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error
stopServerCalled int
stopServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error
stopServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error
deallocateServerCalled int
deallocateServerExecute func(callNo int, ctx context.Context, projectId, serverId string) error
deallocateServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error
getServerCalled int
getServerExecute func(callNo int, ctx context.Context, projectId, serverId string) (*iaas.Server, error)
getServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) (*iaas.Server, error)
}
// DeallocateServerExecute implements serverControlClient.
func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, serverId string) error {
func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, region, serverId string) error {
t.deallocateServerCalled++
return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, serverId)
return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, region, serverId)
}
// GetServerExecute implements serverControlClient.
func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, serverId string) (*iaas.Server, error) {
func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, region, serverId string) (*iaas.Server, error) {
t.getServerCalled++
return t.getServerExecute(t.getServerCalled, ctx, projectId, serverId)
return t.getServerExecute(t.getServerCalled, ctx, projectId, region, serverId)
}
// StartServerExecute implements serverControlClient.
func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, serverId string) error {
func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, region, serverId string) error {
t.startServerCalled++
return t.startServerExecute(t.startServerCalled, ctx, projectId, serverId)
return t.startServerExecute(t.startServerCalled, ctx, projectId, region, serverId)
}
// StopServerExecute implements serverControlClient.
func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, serverId string) error {
func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, region, serverId string) error {
t.stopServerCalled++
return t.stopServerExecute(t.stopServerCalled, ctx, projectId, serverId)
return t.stopServerExecute(t.stopServerCalled, ctx, projectId, region, serverId)
}
func Test_serverResource_updateServerStatus(t *testing.T) {
@ -379,6 +410,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
type args struct {
currentState *string
model Model
region string
}
type want struct {
err bool
@ -398,7 +430,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
name: "no desired status",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) {
getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: utils.Ptr(wait.ServerActiveStatus),
@ -422,7 +454,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
name: "desired inactive state",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(no int, _ context.Context, _, _ string) (*iaas.Server, error) {
getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
var state string
if no <= 1 {
state = wait.ServerActiveStatus
@ -434,7 +466,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
Status: &state,
}, nil
},
stopServerExecute: func(_ int, _ context.Context, _, _ string) error { return nil },
stopServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil },
},
},
args: args{
@ -455,7 +487,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
name: "desired deallocated state",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(no int, _ context.Context, _, _ string) (*iaas.Server, error) {
getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
var state string
switch no {
case 1:
@ -470,7 +502,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
Status: &state,
}, nil
},
deallocateServerExecute: func(_ int, _ context.Context, _, _ string) error { return nil },
deallocateServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil },
},
},
args: args{
@ -491,7 +523,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
name: "don't call start if active",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) {
getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: utils.Ptr(wait.ServerActiveStatus),
@ -516,7 +548,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
name: "don't call stop if inactive",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) {
getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: utils.Ptr(wait.ServerInactiveStatus),
@ -541,7 +573,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
name: "don't call dealloacate if deallocated",
fields: fields{
client: &mockServerControlClient{
getServerExecute: func(_ int, _ context.Context, _, _ string) (*iaas.Server, error) {
getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) {
return &iaas.Server{
Id: utils.Ptr(serverId.ValueString()),
Status: utils.Ptr(wait.ServerDeallocatedStatus),
@ -566,7 +598,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model)
err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model, tt.args.region)
if (err != nil) != tt.want.err {
t.Errorf("inconsistent error, want %v and got %v", tt.want.err, err)
}

View file

@ -11,7 +11,6 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@ -27,41 +26,75 @@ import (
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &networkInterfaceAttachResource{}
_ resource.ResourceWithConfigure = &networkInterfaceAttachResource{}
_ resource.ResourceWithImportState = &networkInterfaceAttachResource{}
_ resource.Resource = &serviceAccountAttachResource{}
_ resource.ResourceWithConfigure = &serviceAccountAttachResource{}
_ resource.ResourceWithImportState = &serviceAccountAttachResource{}
_ resource.ResourceWithModifyPlan = &serviceAccountAttachResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
ServerId types.String `tfsdk:"server_id"`
ServiceAccountEmail types.String `tfsdk:"service_account_email"`
}
// NewServiceAccountAttachResource is a helper function to simplify the provider implementation.
func NewServiceAccountAttachResource() resource.Resource {
return &networkInterfaceAttachResource{}
return &serviceAccountAttachResource{}
}
// networkInterfaceAttachResource is the resource implementation.
type networkInterfaceAttachResource struct {
client *iaas.APIClient
// serviceAccountAttachResource is the resource implementation.
type serviceAccountAttachResource struct {
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
func (r *serviceAccountAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_service_account_attach"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *serviceAccountAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
func (r *serviceAccountAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -70,14 +103,14 @@ func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req reso
}
// Schema defines the schema for the resource.
func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
func (r *serviceAccountAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Service account attachment resource schema. Attaches a service account to a server. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: description,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`service_account_email`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`service_account_email`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -94,6 +127,15 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"server_id": schema.StringAttribute{
Description: "The server ID.",
Required: true,
@ -117,7 +159,7 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc
}
// Create creates the resource and sets the initial Terraform state.
func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
func (r *serviceAccountAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
@ -129,14 +171,16 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
serviceAccountEmail := model.ServiceAccountEmail.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail)
// Create new service account attachment
_, err := r.client.AddServiceAccountToServer(ctx, projectId, serverId, serviceAccountEmail).Execute()
_, err := r.client.AddServiceAccountToServer(ctx, projectId, region, serverId, serviceAccountEmail).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching service account to server", fmt.Sprintf("Calling API: %v", err))
return
@ -144,7 +188,8 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc
ctx = core.LogResponse(ctx)
model.Id = utils.BuildInternalTerraformId(projectId, serverId, serviceAccountEmail)
model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, serviceAccountEmail)
model.Region = types.StringValue(region)
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
@ -156,7 +201,7 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc
}
// Read refreshes the Terraform state with the latest data.
func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
func (r *serviceAccountAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
@ -167,13 +212,15 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
serviceAccountEmail := model.ServiceAccountEmail.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail)
serviceAccounts, err := r.client.ListServerServiceAccounts(ctx, projectId, serverId).Execute()
serviceAccounts, err := r.client.ListServerServiceAccounts(ctx, projectId, region, serverId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -196,6 +243,10 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.
if mail != serviceAccountEmail {
continue
}
model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, serviceAccountEmail)
model.Region = types.StringValue(region)
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
@ -212,12 +263,12 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
func (r *serviceAccountAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Update is not supported, all fields require replace
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
func (r *serviceAccountAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from state
var model Model
diags := req.State.Get(ctx, &model)
@ -229,14 +280,15 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
service_accountId := model.ServiceAccountEmail.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "service_account_email", service_accountId)
// Remove service_account from server
_, err := r.client.RemoveServiceAccountFromServer(ctx, projectId, serverId, service_accountId).Execute()
_, err := r.client.RemoveServiceAccountFromServer(ctx, projectId, region, serverId, service_accountId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing service account from server", fmt.Sprintf("Calling API: %v", err))
return
@ -249,26 +301,23 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,server_id
func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
func (r *serviceAccountAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing service_account attachment",
fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[service_account_email] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[service_account_email] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
serverId := idParts[1]
service_accountId := idParts[2]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "service_account_email", service_accountId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"server_id": idParts[2],
"service_account_email": idParts[3],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_account_email"), service_accountId)...)
tflog.Info(ctx, "Service account attachment state imported")
}

View file

@ -8,8 +8,10 @@ variable "default_prefix_length" {}
variable "max_prefix_length" {}
variable "min_prefix_length" {}
variable "route_prefix" {}
variable "route_next_hop" {}
variable "route_destination_type" {}
variable "route_destination_value" {}
variable "route_next_hop_type" {}
variable "route_next_hop_value" {}
variable "label" {}
resource "stackit_network_area" "network_area" {
@ -33,8 +35,14 @@ resource "stackit_network_area" "network_area" {
resource "stackit_network_area_route" "network_area_route" {
organization_id = stackit_network_area.network_area.organization_id
network_area_id = stackit_network_area.network_area.network_area_id
prefix = var.route_prefix
next_hop = var.route_next_hop
destination = {
type = var.route_destination_type
value = var.route_destination_value
}
next_hop = {
type = var.route_next_hop_type
value = var.route_next_hop_value
}
labels = {
"acc-test" : var.label
}

View file

@ -1,26 +1,8 @@
variable "organization_id" {}
variable "name" {}
variable "transfer_network" {}
variable "network_ranges_prefix" {}
variable "route_prefix" {}
variable "route_next_hop" {}
resource "stackit_network_area" "network_area" {
organization_id = var.organization_id
name = var.name
transfer_network = var.transfer_network
network_ranges = [
{
prefix = var.network_ranges_prefix
}
]
organization_id = var.organization_id
name = var.name
}
resource "stackit_network_area_route" "network_area_route" {
organization_id = stackit_network_area.network_area.organization_id
network_area_id = stackit_network_area.network_area.network_area_id
prefix = var.route_prefix
next_hop = var.route_next_hop
}

View file

@ -0,0 +1,33 @@
variable "organization_id" {}
variable "name" {}
variable "transfer_network" {}
variable "network_ranges_prefix" {}
variable "default_prefix_length" {}
variable "min_prefix_length" {}
variable "max_prefix_length" {}
variable "default_nameservers" {}
resource "stackit_network_area" "network_area" {
organization_id = var.organization_id
name = var.name
}
resource "stackit_network_area_region" "network_area_region" {
organization_id = var.organization_id
network_area_id = stackit_network_area.network_area.network_area_id
ipv4 = {
transfer_network = var.transfer_network
network_ranges = [
{
prefix = var.network_ranges_prefix
}
]
default_prefix_length = var.default_prefix_length
min_prefix_length = var.min_prefix_length
max_prefix_length = var.max_prefix_length
default_nameservers = [
var.default_nameservers
]
}
}

View file

@ -0,0 +1,23 @@
variable "organization_id" {}
variable "name" {}
variable "transfer_network" {}
variable "network_ranges_prefix" {}
resource "stackit_network_area" "network_area" {
organization_id = var.organization_id
name = var.name
}
resource "stackit_network_area_region" "network_area_region" {
organization_id = var.organization_id
network_area_id = stackit_network_area.network_area.network_area_id
ipv4 = {
transfer_network = var.transfer_network
network_ranges = [
{
prefix = var.network_ranges_prefix
}
]
}
}

View file

@ -0,0 +1,85 @@
variable "organization_id" {}
variable "name" {}
variable "ipv4_gateway" {}
variable "ipv4_nameserver_0" {}
variable "ipv4_nameserver_1" {}
variable "ipv4_prefix" {}
variable "ipv4_prefix_length" {}
variable "routed" {}
variable "label" {}
variable "service_account_mail" {}
# no test candidate, just needed for the testing setup
resource "stackit_network_area" "network_area" {
organization_id = var.organization_id
name = var.name
labels = {
"preview/routingtables" = "true"
}
}
resource "stackit_network_area_region" "network_area_region" {
organization_id = var.organization_id
network_area_id = stackit_network_area.network_area.network_area_id
ipv4 = {
network_ranges = [
{
prefix = "10.0.0.0/16"
},
{
prefix = "10.2.2.0/24"
}
]
transfer_network = "10.1.2.0/24"
}
}
# no test candidate, just needed for the testing setup
resource "stackit_resourcemanager_project" "project" {
parent_container_id = stackit_network_area.network_area.organization_id
name = var.name
labels = {
"networkArea" = stackit_network_area.network_area.network_area_id
}
owner_email = var.service_account_mail
depends_on = [stackit_network_area_region.network_area_region]
}
resource "stackit_network" "network_prefix" {
project_id = stackit_resourcemanager_project.project.project_id
name = var.name
# ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null
# no_ipv4_gateway = var.ipv4_gateway != "" ? null : true
ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1]
ipv4_prefix = var.ipv4_prefix
routed = var.routed
labels = {
"acc-test" : var.label
}
depends_on = [stackit_network_area_region.network_area_region]
}
resource "stackit_network" "network_prefix_length" {
project_id = stackit_resourcemanager_project.project.project_id
name = var.name
# no_ipv4_gateway = true
ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1]
ipv4_prefix_length = var.ipv4_prefix_length
routed = var.routed
labels = {
"acc-test" : var.label
}
routing_table_id = stackit_routing_table.routing_table.routing_table_id
depends_on = [stackit_network.network_prefix, stackit_network_area_region.network_area_region]
}
resource "stackit_routing_table" "routing_table" {
organization_id = var.organization_id
network_area_id = stackit_network_area.network_area.network_area_id
name = var.name
depends_on = [stackit_network_area_region.network_area_region]
}

View file

@ -1,35 +0,0 @@
variable "project_id" {}
variable "name" {}
variable "ipv4_gateway" {}
variable "ipv4_nameserver_0" {}
variable "ipv4_nameserver_1" {}
variable "ipv4_prefix" {}
variable "ipv4_prefix_length" {}
variable "routed" {}
variable "label" {}
resource "stackit_network" "network_prefix" {
project_id = var.project_id
name = var.name
ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null
no_ipv4_gateway = var.ipv4_gateway != "" ? null : true
ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1]
ipv4_prefix = var.ipv4_prefix
routed = var.routed
labels = {
"acc-test" : var.label
}
}
resource "stackit_network" "network_prefix_length" {
project_id = var.project_id
name = var.name
no_ipv4_gateway = true
ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1]
ipv4_prefix_length = var.ipv4_prefix_length
routed = var.routed
labels = {
"acc-test" : var.label
}
}

View file

@ -1,43 +0,0 @@
variable "project_id" {}
variable "name" {}
variable "ipv4_gateway" {}
variable "ipv4_nameserver_0" {}
variable "ipv4_nameserver_1" {}
variable "ipv4_prefix" {}
variable "ipv4_prefix_length" {}
variable "routed" {}
variable "label" {}
variable "organization_id" {}
variable "network_area_id" {}
# resource "stackit_network" "network_prefix" {
# project_id = var.project_id
# name = var.name
# # ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null
# # no_ipv4_gateway = var.ipv4_gateway != "" ? null : true
# ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1]
# ipv4_prefix = var.ipv4_prefix
# routed = var.routed
# labels = {
# "acc-test" : var.label
# }
# }
resource "stackit_network" "network_prefix_length" {
project_id = var.project_id
name = var.name
# no_ipv4_gateway = true
ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1]
ipv4_prefix_length = var.ipv4_prefix_length
routed = var.routed
labels = {
"acc-test" : var.label
}
routing_table_id = stackit_routing_table.routing_table.routing_table_id
}
resource "stackit_routing_table" "routing_table" {
organization_id = var.organization_id
network_area_id = var.network_area_id
name = var.name
}

View file

@ -1,7 +0,0 @@
variable "project_id" {}
variable "name" {}
resource "stackit_network" "network" {
project_id = var.project_id
name = var.name
}

View file

@ -1,8 +1,18 @@
variable "project_id" {}
variable "name" {}
variable "network_name" {}
variable "machine_type" {}
variable "image_id" {}
resource "stackit_network" "network" {
project_id = var.project_id
name = var.network_name
}
resource "stackit_network_interface" "nic" {
project_id = var.project_id
network_id = stackit_network.network.network_id
}
resource "stackit_server" "server" {
project_id = var.project_id
@ -14,4 +24,7 @@ resource "stackit_server" "server" {
source_id = var.image_id
delete_on_termination = true
}
network_interfaces = [
stackit_network_interface.nic.network_interface_id
]
}

View file

@ -23,8 +23,9 @@ resource "stackit_volume" "volume_source" {
availability_zone = var.availability_zone
name = var.name
description = var.description
performance_class = var.performance_class
size = var.size
# TODO: keep commented until IaaS API bug is resolved
#performance_class = var.performance_class
size = var.size
source = {
id = stackit_volume.volume_size.volume_id
type = "volume"

View file

@ -21,9 +21,8 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags
}
if providerData.IaaSCustomEndpoint != "" {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.IaaSCustomEndpoint))
} else {
apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion()))
}
apiClient, err := iaas.NewAPIClient(apiClientConfigOptions...)
if err != nil {
core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))

View file

@ -49,7 +49,6 @@ func TestConfigureClient(t *testing.T) {
},
expected: func() *iaas.APIClient {
apiClient, err := iaas.NewAPIClient(
config.WithRegion("eu01"),
utils.UserAgentConfigOption(testVersion),
)
if err != nil {

View file

@ -31,7 +31,8 @@ func NewVolumeDataSource() datasource.DataSource {
// volumeDataSource is the data source implementation.
type volumeDataSource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the data source type name.
@ -40,12 +41,13 @@ func (d *volumeDataSource) Metadata(_ context.Context, req datasource.MetadataRe
}
func (d *volumeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -54,14 +56,14 @@ func (d *volumeDataSource) Configure(ctx context.Context, req datasource.Configu
}
// Schema defines the schema for the resource.
func (r *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
func (d *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "Volume resource schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: description,
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`volume_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`volume_id`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
@ -72,6 +74,11 @@ func (r *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
// the region cannot be found, so it has to be passed
Optional: true,
},
"volume_id": schema.StringAttribute{
Description: "The volume ID.",
Required: true,
@ -140,14 +147,16 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest,
return
}
projectId := model.ProjectId.ValueString()
region := d.providerData.GetRegionWithOverride(model.Region)
volumeId := model.VolumeId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
volumeResp, err := d.client.GetVolume(ctx, projectId, volumeId).Execute()
volumeResp, err := d.client.GetVolume(ctx, projectId, region, volumeId).Execute()
if err != nil {
utils.LogError(
ctx,
@ -165,7 +174,7 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest,
ctx = core.LogResponse(ctx)
err = mapFields(ctx, volumeResp, &model)
err = mapFields(ctx, volumeResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err))
return

View file

@ -37,6 +37,7 @@ var (
_ resource.Resource = &volumeResource{}
_ resource.ResourceWithConfigure = &volumeResource{}
_ resource.ResourceWithImportState = &volumeResource{}
_ resource.ResourceWithModifyPlan = &volumeResource{}
SupportedSourceTypes = []string{"volume", "image", "snapshot", "backup"}
)
@ -44,6 +45,7 @@ var (
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
VolumeId types.String `tfsdk:"volume_id"`
Name types.String `tfsdk:"name"`
AvailabilityZone types.String `tfsdk:"availability_zone"`
@ -74,7 +76,8 @@ func NewVolumeResource() resource.Resource {
// volumeResource is the resource implementation.
type volumeResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -82,6 +85,36 @@ func (r *volumeResource) Metadata(_ context.Context, req resource.MetadataReques
resp.TypeName = req.ProviderTypeName + "_volume"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *volumeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// ConfigValidators validates the resource configuration
func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
return []resource.ConfigValidator{
@ -94,12 +127,13 @@ func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigVa
// Configure adds the provider configured client to the resource.
func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -115,7 +149,7 @@ func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, res
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`volume_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`volume_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -132,6 +166,15 @@ func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, res
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"volume_id": schema.StringAttribute{
Description: "The volume ID.",
Computed: true,
@ -290,7 +333,9 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest,
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
var source = &sourceModel{}
if !(model.Source.IsNull() || model.Source.IsUnknown()) {
@ -310,7 +355,7 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest,
// Create new volume
volume, err := r.client.CreateVolume(ctx, projectId).CreateVolumePayload(*payload).Execute()
volume, err := r.client.CreateVolume(ctx, projectId, region).CreateVolumePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Calling API: %v", err))
return
@ -319,7 +364,7 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest,
ctx = core.LogResponse(ctx)
volumeId := *volume.Id
volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx)
volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, region, volumeId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("volume creation waiting: %v", err))
return
@ -328,7 +373,7 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest,
ctx = tflog.SetField(ctx, "volume_id", volumeId)
// Map response body to schema
err = mapFields(ctx, volume, &model)
err = mapFields(ctx, volume, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Processing API payload: %v", err))
return
@ -350,15 +395,18 @@ func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, res
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
volumeId := model.VolumeId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
volumeResp, err := r.client.GetVolume(ctx, projectId, volumeId).Execute()
volumeResp, err := r.client.GetVolume(ctx, projectId, region, volumeId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -372,7 +420,7 @@ func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, res
ctx = core.LogResponse(ctx)
// Map response body to schema
err = mapFields(ctx, volumeResp, &model)
err = mapFields(ctx, volumeResp, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err))
return
@ -399,8 +447,10 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest,
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
volumeId := model.VolumeId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
// Retrieve values from state
@ -418,7 +468,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest,
return
}
// Update existing volume
updatedVolume, err := r.client.UpdateVolume(ctx, projectId, volumeId).UpdateVolumePayload(*payload).Execute()
updatedVolume, err := r.client.UpdateVolume(ctx, projectId, region, volumeId).UpdateVolumePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Calling API: %v", err))
return
@ -436,7 +486,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest,
payload := iaas.ResizeVolumePayload{
Size: modelSize,
}
err := r.client.ResizeVolume(ctx, projectId, volumeId).ResizeVolumePayload(payload).Execute()
err := r.client.ResizeVolume(ctx, projectId, region, volumeId).ResizeVolumePayload(payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Resizing the volume, calling API: %v", err))
}
@ -444,7 +494,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest,
updatedVolume.Size = modelSize
}
}
err = mapFields(ctx, updatedVolume, &model)
err = mapFields(ctx, updatedVolume, &model, region)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Processing API payload: %v", err))
return
@ -468,15 +518,17 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
projectId := model.ProjectId.ValueString()
region := r.providerData.GetRegionWithOverride(model.Region)
volumeId := model.VolumeId.ValueString()
ctx = core.InitProviderContext(ctx)
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
// Delete existing volume
err := r.client.DeleteVolume(ctx, projectId, volumeId).Execute()
err := r.client.DeleteVolume(ctx, projectId, region, volumeId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("Calling API: %v", err))
return
@ -484,7 +536,7 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest,
ctx = core.LogResponse(ctx)
_, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx)
_, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, region, volumeId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("volume deletion waiting: %v", err))
return
@ -498,25 +550,24 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest,
func (r *volumeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing volume",
fmt.Sprintf("Expected import identifier with format: [project_id],[volume_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[volume_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
volumeId := idParts[1]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"volume_id": idParts[2],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("volume_id"), volumeId)...)
tflog.Info(ctx, "volume state imported")
}
func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error {
func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model, region string) error {
if volumeResp == nil {
return fmt.Errorf("response input is nil")
}
@ -533,7 +584,8 @@ func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error
return fmt.Errorf("Volume id not present")
}
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), volumeId)
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, volumeId)
model.Region = types.StringValue(region)
labels, err := iaasUtils.MapLabels(ctx, volumeResp.Labels, model.Labels)
if err != nil {

View file

@ -12,24 +12,31 @@ import (
)
func TestMapFields(t *testing.T) {
type args struct {
state Model
input *iaas.Volume
region string
}
tests := []struct {
description string
state Model
input *iaas.Volume
args args
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
description: "default_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
},
input: &iaas.Volume{
Id: utils.Ptr("nid"),
},
region: "eu01",
},
&iaas.Volume{
Id: utils.Ptr("nid"),
},
Model{
Id: types.StringValue("pid,nid"),
expected: Model{
Id: types.StringValue("pid,eu01,nid"),
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
Name: types.StringNull(),
@ -40,30 +47,35 @@ func TestMapFields(t *testing.T) {
ServerId: types.StringNull(),
Size: types.Int64Null(),
Source: types.ObjectNull(sourceTypes),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"simple_values",
Model{
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
},
&iaas.Volume{
Id: utils.Ptr("nid"),
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
description: "simple_values",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
Region: types.StringValue("eu01"),
},
Description: utils.Ptr("desc"),
PerformanceClass: utils.Ptr("class"),
ServerId: utils.Ptr("sid"),
Size: utils.Ptr(int64(1)),
Source: &iaas.VolumeSource{},
input: &iaas.Volume{
Id: utils.Ptr("nid"),
Name: utils.Ptr("name"),
AvailabilityZone: utils.Ptr("zone"),
Labels: &map[string]interface{}{
"key": "value",
},
Description: utils.Ptr("desc"),
PerformanceClass: utils.Ptr("class"),
ServerId: utils.Ptr("sid"),
Size: utils.Ptr(int64(1)),
Source: &iaas.VolumeSource{},
},
region: "eu02",
},
Model{
Id: types.StringValue("pid,nid"),
expected: Model{
Id: types.StringValue("pid,eu02,nid"),
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
Name: types.StringValue("name"),
@ -79,21 +91,25 @@ func TestMapFields(t *testing.T) {
"type": types.StringNull(),
"id": types.StringNull(),
}),
Region: types.StringValue("eu02"),
},
true,
isValid: true,
},
{
"empty_labels",
Model{
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
description: "empty_labels",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
},
input: &iaas.Volume{
Id: utils.Ptr("nid"),
},
region: "eu01",
},
&iaas.Volume{
Id: utils.Ptr("nid"),
},
Model{
Id: types.StringValue("pid,nid"),
expected: Model{
Id: types.StringValue("pid,eu01,nid"),
ProjectId: types.StringValue("pid"),
VolumeId: types.StringValue("nid"),
Name: types.StringNull(),
@ -104,29 +120,28 @@ func TestMapFields(t *testing.T) {
ServerId: types.StringNull(),
Size: types.Int64Null(),
Source: types.ObjectNull(sourceTypes),
Region: types.StringValue("eu01"),
},
true,
isValid: true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
description: "response_nil_fail",
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
description: "no_resource_id",
args: args{
state: Model{
ProjectId: types.StringValue("pid"),
},
input: &iaas.Volume{},
},
&iaas.Volume{},
Model{},
false,
expected: Model{},
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
@ -134,7 +149,7 @@ func TestMapFields(t *testing.T) {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
diff := cmp.Diff(tt.args.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}

View file

@ -11,7 +11,6 @@ import (
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
@ -32,11 +31,13 @@ var (
_ resource.Resource = &volumeAttachResource{}
_ resource.ResourceWithConfigure = &volumeAttachResource{}
_ resource.ResourceWithImportState = &volumeAttachResource{}
_ resource.ResourceWithModifyPlan = &volumeAttachResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
Region types.String `tfsdk:"region"`
ServerId types.String `tfsdk:"server_id"`
VolumeId types.String `tfsdk:"volume_id"`
}
@ -48,7 +49,8 @@ func NewVolumeAttachResource() resource.Resource {
// volumeAttachResource is the resource implementation.
type volumeAttachResource struct {
client *iaas.APIClient
client *iaas.APIClient
providerData core.ProviderData
}
// Metadata returns the resource type name.
@ -56,14 +58,45 @@ func (r *volumeAttachResource) Metadata(_ context.Context, req resource.Metadata
resp.TypeName = req.ProviderTypeName + "_server_volume_attach"
}
// ModifyPlan implements resource.ResourceWithModifyPlan.
// Use the modifier to set the effective region in the current plan.
func (r *volumeAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
var configModel Model
// skip initial empty configuration to avoid follow-up errors
if req.Config.Raw.IsNull() {
return
}
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
if resp.Diagnostics.HasError() {
return
}
var planModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...)
if resp.Diagnostics.HasError() {
return
}
utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...)
if resp.Diagnostics.HasError() {
return
}
}
// Configure adds the provider configured client to the resource.
func (r *volumeAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
var ok bool
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
if !ok {
return
}
apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}
@ -79,7 +112,7 @@ func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaReques
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`volume_id`\".",
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`volume_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
@ -96,6 +129,15 @@ func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaReques
validate.NoSeparator(),
},
},
"region": schema.StringAttribute{
Description: "The resource region. If not defined, the provider region is used.",
Optional: true,
// must be computed to allow for storing the override value from the provider
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"server_id": schema.StringAttribute{
Description: "The server ID.",
Required: true,
@ -135,10 +177,12 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
volumeId := model.VolumeId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
// Create new Volume attachment
@ -146,7 +190,7 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe
payload := iaas.AddVolumeToServerPayload{
DeleteOnTermination: sdkUtils.Ptr(false),
}
_, err := r.client.AddVolumeToServer(ctx, projectId, serverId, volumeId).AddVolumeToServerPayload(payload).Execute()
_, err := r.client.AddVolumeToServer(ctx, projectId, region, serverId, volumeId).AddVolumeToServerPayload(payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("Calling API: %v", err))
return
@ -154,13 +198,13 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe
ctx = core.LogResponse(ctx)
_, err = wait.AddVolumeToServerWaitHandler(ctx, r.client, projectId, serverId, volumeId).WaitWithContext(ctx)
_, err = wait.AddVolumeToServerWaitHandler(ctx, r.client, projectId, region, serverId, volumeId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("volume attachment waiting: %v", err))
return
}
model.Id = utils.BuildInternalTerraformId(projectId, serverId, volumeId)
model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, volumeId)
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
@ -183,13 +227,15 @@ func (r *volumeAttachResource) Read(ctx context.Context, req resource.ReadReques
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
volumeId := model.VolumeId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
_, err := r.client.GetAttachedVolume(ctx, projectId, serverId, volumeId).Execute()
_, err := r.client.GetAttachedVolume(ctx, projectId, region, serverId, volumeId).Execute()
if err != nil {
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
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -229,14 +275,16 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe
ctx = core.InitProviderContext(ctx)
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
region := r.providerData.GetRegionWithOverride(model.Region)
serverId := model.ServerId.ValueString()
ctx = tflog.SetField(ctx, "server_id", serverId)
volumeId := model.VolumeId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
// Remove volume from server
err := r.client.RemoveVolumeFromServer(ctx, projectId, serverId, volumeId).Execute()
err := r.client.RemoveVolumeFromServer(ctx, projectId, region, serverId, volumeId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("Calling API: %v", err))
return
@ -244,7 +292,7 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe
ctx = core.LogResponse(ctx)
_, err = wait.RemoveVolumeFromServerWaitHandler(ctx, r.client, projectId, serverId, volumeId).WaitWithContext(ctx)
_, err = wait.RemoveVolumeFromServerWaitHandler(ctx, r.client, projectId, region, serverId, volumeId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("volume removal waiting: %v", err))
return
@ -258,23 +306,20 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe
func (r *volumeAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing volume attachment",
fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[volume_id] Got: %q", req.ID),
fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[volume_id] Got: %q", req.ID),
)
return
}
projectId := idParts[0]
serverId := idParts[1]
volumeId := idParts[2]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "server_id", serverId)
ctx = tflog.SetField(ctx, "volume_id", volumeId)
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": idParts[0],
"region": idParts[1],
"server_id": idParts[2],
"volume_id": idParts[3],
})
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("volume_id"), volumeId)...)
tflog.Info(ctx, "Volume attachment state imported")
}

View file

@ -176,6 +176,9 @@ func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.CIDR(),
},
},
},
},
@ -213,6 +216,9 @@ func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.IP(false),
},
},
},
},

View file

@ -261,7 +261,7 @@ func TestAccLogMeMaxResource(t *testing.T) {
resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog1"])),
resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog2"])),
// // Credential data
// Credential data
resource.TestCheckResourceAttrPair(
"stackit_logme_credential.credential", "project_id",
"stackit_logme_instance.instance", "project_id",

View file

@ -8,6 +8,7 @@ import (
"strings"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils"
@ -528,16 +529,25 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
Description: "Specifies for how many days the raw metrics are kept. Default is set to `90`.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"metrics_retention_days_5m_downsampling": schema.Int64Attribute{
Description: "Specifies for how many days the 5m downsampled metrics are kept. must be less than the value of the general retention. Default is set to `90`.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"metrics_retention_days_1h_downsampling": schema.Int64Attribute{
Description: "Specifies for how many days the 1h downsampled metrics are kept. must be less than the value of the 5m downsampling retention. Default is set to `90`.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
},
"metrics_url": schema.StringAttribute{
Description: "Specifies metrics URL.",
@ -659,6 +669,8 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
"send_resolved": schema.BoolAttribute{
Description: "Whether to notify about resolved alerts.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"smart_host": schema.StringAttribute{
Description: "The SMTP host through which emails are sent.",
@ -698,6 +710,8 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
"send_resolved": schema.BoolAttribute{
Description: "Whether to notify about resolved alerts.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
},
},
@ -733,6 +747,8 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
"send_resolved": schema.BoolAttribute{
Description: "Whether to notify about resolved alerts.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
},
},
@ -789,10 +805,18 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
Description: "The API key for OpsGenie.",
Optional: true,
Sensitive: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"opsgenie_api_url": schema.StringAttribute{
Description: "The host to send OpsGenie API requests to. Must be a valid URL",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"resolve_timeout": schema.StringAttribute{
Description: "The default value used by alertmanager if the alert does not include EndsAt. After this time passes, it can declare the alert as resolved if it has not been updated. This has no impact on alerts from Prometheus, as they always include EndsAt.",
@ -805,24 +829,43 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
"smtp_auth_identity": schema.StringAttribute{
Description: "SMTP authentication information. Must be a valid email address",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"smtp_auth_password": schema.StringAttribute{
Description: "SMTP Auth using LOGIN and PLAIN.",
Optional: true,
Sensitive: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"smtp_auth_username": schema.StringAttribute{
Description: "SMTP Auth using CRAM-MD5, LOGIN and PLAIN. If empty, Alertmanager doesn't authenticate to the SMTP server.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"smtp_from": schema.StringAttribute{
Description: "The default SMTP From header field. Must be a valid email address",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"smtp_smart_host": schema.StringAttribute{
Description: "The default SMTP smarthost used for sending emails, including port number in format `host:port` (eg. `smtp.example.com:587`). Port number usually is 25, or 587 for SMTP over TLS (sometimes referred to as STARTTLS).",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
},
@ -1798,6 +1841,8 @@ func mapGlobalConfigToAttributes(respGlobalConfigs *observability.Global, global
smtpAuthIdentity := respGlobalConfigs.SmtpAuthIdentity
smtpAuthPassword := respGlobalConfigs.SmtpAuthPassword
smtpAuthUsername := respGlobalConfigs.SmtpAuthUsername
opsgenieApiKey := respGlobalConfigs.OpsgenieApiKey
opsgenieApiUrl := respGlobalConfigs.OpsgenieApiUrl
if globalConfigsTF != nil {
if respGlobalConfigs.SmtpSmarthost == nil &&
!globalConfigsTF.SmtpSmartHost.IsNull() && !globalConfigsTF.SmtpSmartHost.IsUnknown() {
@ -1815,11 +1860,17 @@ func mapGlobalConfigToAttributes(respGlobalConfigs *observability.Global, global
!globalConfigsTF.SmtpAuthUsername.IsNull() && !globalConfigsTF.SmtpAuthUsername.IsUnknown() {
smtpAuthUsername = sdkUtils.Ptr(globalConfigsTF.SmtpAuthUsername.ValueString())
}
if respGlobalConfigs.OpsgenieApiKey == nil {
opsgenieApiKey = sdkUtils.Ptr(globalConfigsTF.OpsgenieApiKey.ValueString())
}
if respGlobalConfigs.OpsgenieApiUrl == nil {
opsgenieApiUrl = sdkUtils.Ptr(globalConfigsTF.OpsgenieApiUrl.ValueString())
}
}
globalConfigObject, diags := types.ObjectValue(globalConfigurationTypes, map[string]attr.Value{
"opsgenie_api_key": types.StringPointerValue(respGlobalConfigs.OpsgenieApiKey),
"opsgenie_api_url": types.StringPointerValue(respGlobalConfigs.OpsgenieApiUrl),
"opsgenie_api_key": types.StringPointerValue(opsgenieApiKey),
"opsgenie_api_url": types.StringPointerValue(opsgenieApiUrl),
"resolve_timeout": types.StringPointerValue(respGlobalConfigs.ResolveTimeout),
"smtp_from": types.StringPointerValue(respGlobalConfigs.SmtpFrom),
"smtp_auth_identity": types.StringPointerValue(smtpAuthIdentity),

View file

@ -121,7 +121,7 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) {
resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "id"),
),
},
// // Import
// Import
{
ConfigVariables: testConfigVarsMin,
ResourceName: "stackit_server_update_schedule.test_schedule",
@ -139,7 +139,7 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) {
ImportState: true,
ImportStateVerify: true,
},
// // Update
// Update
{
ConfigVariables: configVarsMinUpdated(),
Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig,
@ -209,7 +209,7 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) {
resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "id"),
),
},
// // Import
// Import
{
ConfigVariables: testConfigVarsMax,
ResourceName: "stackit_server_update_schedule.test_schedule",
@ -227,7 +227,7 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) {
ImportState: true,
ImportStateVerify: true,
},
// // Update
// Update
{
ConfigVariables: configVarsMaxUpdated(),
Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig,

View file

@ -33,6 +33,7 @@ import (
machineType "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/machinetype"
iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network"
iaasNetworkArea "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarea"
iaasNetworkAreaRegion "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearegion"
iaasNetworkAreaRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearoute"
iaasNetworkInterface "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterface"
iaasNetworkInterfaceAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterfaceattach"
@ -502,6 +503,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
iaasImageV2.NewImageV2DataSource,
iaasNetwork.NewNetworkDataSource,
iaasNetworkArea.NewNetworkAreaDataSource,
iaasNetworkAreaRegion.NewNetworkAreaRegionDataSource,
iaasNetworkAreaRoute.NewNetworkAreaRouteDataSource,
iaasNetworkInterface.NewNetworkInterfaceDataSource,
iaasVolume.NewVolumeDataSource,
@ -573,6 +575,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
iaasImage.NewImageResource,
iaasNetwork.NewNetworkResource,
iaasNetworkArea.NewNetworkAreaResource,
iaasNetworkAreaRegion.NewNetworkAreaRegionResource,
iaasNetworkAreaRoute.NewNetworkAreaRouteResource,
iaasNetworkInterface.NewNetworkInterfaceResource,
iaasVolume.NewVolumeResource,