feat: Onboard affinity groups resource and data source (#652)

* onboard affinity_groups resource and data source
- add tests and descriptions
- fix: server doesn't use affinity_group value for payload

* Update descriptions
This commit is contained in:
Marcel Jacek 2025-01-30 11:07:32 +01:00 committed by GitHub
parent 3642260cc4
commit b5ce160d13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 846 additions and 0 deletions

View file

@ -0,0 +1,35 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_affinity_group Data Source - stackit"
subcategory: ""
description: |-
Affinity Group schema. Must have a region specified in the provider configuration.
---
# stackit_affinity_group (Data Source)
Affinity Group schema. Must have a `region` specified in the provider configuration.
## Example Usage
```terraform
data "stackit_affinity_group" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
affinity_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `affinity_group_id` (String) The affinity group ID.
- `project_id` (String) STACKIT Project ID to which the affinity group is associated.
### Read-Only
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`affinity_group_id`".
- `members` (List of String) Affinity Group schema. Must have a `region` specified in the provider configuration.
- `name` (String) The name of the affinity group.
- `policy` (String) The policy of the affinity group.

View file

@ -0,0 +1,103 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_affinity_group Resource - stackit"
subcategory: ""
description: |-
Affinity Group schema. Must have a region specified in the provider configuration.
Usage with server
```terraform
resource "stackitaffinitygroup" "affinity-group" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-key-pair"
policy = "soft-affinity"
}
resource "stackitserver" "example-server" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
bootvolume = {
size = 64
sourcetype = "image"
sourceid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
affinitygroup = stackitaffinitygroup.affinity-group.affinitygroupid
availabilityzone = "eu01-1"
machinetype = "g1.1"
}
```
Policies
hard-affinity- All servers launched in this group will be hosted on the same compute node.hard-anti-affinity- All servers launched in this group will be
hosted on different compute nodes.soft-affinity- All servers launched in this group will be hosted
on the same compute node if possible, but if not possible they still will be scheduled instead of failure.soft-anti-affinity- All servers launched in this group will be hosted on different compute nodes if possible,
but if not possible they still will be scheduled instead of failure.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
---
# stackit_affinity_group (Resource)
Affinity Group schema. Must have a `region` specified in the provider configuration.
### Usage with server
```terraform
resource "stackit_affinity_group" "affinity-group" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-key-pair"
policy = "soft-affinity"
}
resource "stackit_server" "example-server" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
boot_volume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
affinity_group = stackit_affinity_group.affinity-group.affinity_group_id
availability_zone = "eu01-1"
machine_type = "g1.1"
}
```
### Policies
* `hard-affinity`- All servers launched in this group will be hosted on the same compute node.
* `hard-anti-affinity`- All servers launched in this group will be
hosted on different compute nodes.
* `soft-affinity`- All servers launched in this group will be hosted
on the same compute node if possible, but if not possible they still will be scheduled instead of failure.
* `soft-anti-affinity`- All servers launched in this group will be hosted on different compute nodes if possible,
but if not possible they still will be scheduled instead of failure.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
## Example Usage
```terraform
resource "stackit_affinity_group" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-affinity-group-name"
policy = "hard-anti-affinity"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The name of the affinity group.
- `policy` (String) The policy of the affinity group.
- `project_id` (String) STACKIT Project ID to which the affinity group is associated.
### Read-Only
- `affinity_group_id` (String) The affinity group ID.
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`affinity_group_id`".
- `members` (List of String) The servers that are part of the affinity group.

View file

@ -0,0 +1,4 @@
data "stackit_affinity_group" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
affinity_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

View file

@ -0,0 +1,5 @@
resource "stackit_affinity_group" "example" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-affinity-group-name"
policy = "hard-anti-affinity"
}

View file

@ -0,0 +1,41 @@
package affinitygroup
const exampleUsageWithServer = `
### Usage with server` + "\n" +
"```terraform" + `
resource "stackit_affinity_group" "affinity-group" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-key-pair"
policy = "soft-affinity"
}
resource "stackit_server" "example-server" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
boot_volume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
affinity_group = stackit_affinity_group.affinity-group.affinity_group_id
availability_zone = "eu01-1"
machine_type = "g1.1"
}
` + "\n```"
const policies = `
### Policies
* ` + "`hard-affinity`" + `- All servers launched in this group will be hosted on the same compute node.
* ` + "`hard-anti-affinity`" + `- All servers launched in this group will be
hosted on different compute nodes.
* ` + "`soft-affinity`" + `- All servers launched in this group will be hosted
on the same compute node if possible, but if not possible they still will be scheduled instead of failure.
* ` + "`soft-anti-affinity`" + `- All servers launched in this group will be hosted on different compute nodes if possible,
but if not possible they still will be scheduled instead of failure.
`

View file

@ -0,0 +1,177 @@
package affinitygroup
import (
"context"
"fmt"
"net/http"
"regexp"
"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/validate"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
var affinityGroupDataSourceBetaCheckDone bool
var (
_ datasource.DataSource = &affinityGroupDatasource{}
_ datasource.DataSourceWithConfigure = &affinityGroupDatasource{}
)
func NewAffinityGroupDatasource() datasource.DataSource {
return &affinityGroupDatasource{}
}
type affinityGroupDatasource struct {
client *iaas.APIClient
}
func (d *affinityGroupDatasource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
if !affinityGroupDataSourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_affinity_group", "data source")
if resp.Diagnostics.HasError() {
return
}
affinityGroupDataSourceBetaCheckDone = true
}
var apiClient *iaas.APIClient
var err error
if providerData.IaaSCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
apiClient, err = iaas.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.IaaSCustomEndpoint),
)
} else {
apiClient, err = iaas.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.Region),
)
}
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuratio", err))
return
}
d.client = apiClient
tflog.Info(ctx, "iaas client configured")
}
func (d *affinityGroupDatasource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_affinity_group"
}
func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
descriptionMain := "Affinity Group schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
Description: descriptionMain,
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`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
Description: "STACKIT Project ID to which the affinity group is associated.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"affinity_group_id": schema.StringAttribute{
Description: "The affinity group ID.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"name": schema.StringAttribute{
Description: "The name of the affinity group.",
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.LengthAtMost(63),
stringvalidator.RegexMatches(
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
"must match expression"),
},
},
"policy": schema.StringAttribute{
Description: "The policy of the affinity group.",
Computed: true,
},
"members": schema.ListAttribute{
Description: descriptionMain,
Computed: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(
validate.UUID(),
),
},
},
},
}
}
func (d *affinityGroupDatasource) 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...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
affinityGroupId := model.AffinityGroupId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId)
affinityGroupResp, err := d.client.GetAffinityGroupExecute(ctx, projectId, 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 {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Call API: %v", err))
return
}
err = mapFields(ctx, affinityGroupResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Processing API payload: %v", err))
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Affinity group read")
}

View file

@ -0,0 +1,363 @@
package affinitygroup
import (
"context"
"fmt"
"net/http"
"regexp"
"strings"
"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/validate"
"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"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"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/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
// affinityGroupResourceBetaCheckDone is used to prevent multiple checks for beta resources.
// This is a workaround for the lack of a global state in the provider and
// needs to exist because the Configure method is called twice.
var affinityGroupResourceBetaCheckDone bool
var (
_ resource.Resource = &affinityGroupResource{}
_ resource.ResourceWithConfigure = &affinityGroupResource{}
_ resource.ResourceWithImportState = &affinityGroupResource{}
)
// Model is the provider's internal model
type Model struct {
Id types.String `tfsdk:"id"`
ProjectId types.String `tfsdk:"project_id"`
AffinityGroupId types.String `tfsdk:"affinity_group_id"`
Name types.String `tfsdk:"name"`
Policy types.String `tfsdk:"policy"`
Members types.List `tfsdk:"members"`
}
func NewAffinityGroupResource() resource.Resource {
return &affinityGroupResource{}
}
// affinityGroupResource is the resource implementation.
type affinityGroupResource struct {
client *iaas.APIClient
}
// Metadata returns the resource type name.
func (r *affinityGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_affinity_group"
}
// Configure adds the provider configured client to the resource.
func (r *affinityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderDate, got %T", req.ProviderData))
return
}
if !affinityGroupResourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_affinity_group", "resource")
if resp.Diagnostics.HasError() {
return
}
affinityGroupResourceBetaCheckDone = true
}
var apiClient *iaas.APIClient
var err error
if providerData.IaaSCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
apiClient, err = iaas.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.IaaSCustomEndpoint),
)
} else {
apiClient, err = iaas.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.Region),
)
}
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuratio", err))
return
}
r.client = apiClient
tflog.Info(ctx, "iaas client configured")
}
func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Affinity Group schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
Description: description,
MarkdownDescription: features.AddBetaDescription(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`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"project_id": schema.StringAttribute{
Description: "STACKIT Project ID to which the affinity group is associated.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"affinity_group_id": schema.StringAttribute{
Description: "The affinity group ID.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"name": schema.StringAttribute{
Description: "The name of the affinity group.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.LengthAtMost(63),
stringvalidator.RegexMatches(
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
"must match expression"),
},
},
"policy": schema.StringAttribute{
Description: "The policy of the affinity group.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{},
},
"members": schema.ListAttribute{
Description: "The servers that are part of the affinity group.",
Computed: true,
ElementType: types.StringType,
Validators: []validator.List{
listvalidator.ValueStringsAre(
validate.UUID(),
),
},
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
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()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupResp.Id)
// Map response body to schema
err = mapFields(ctx, affinityGroupResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", 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, "Affinity group created")
}
// Read refreshes the Terraform state with the latest data.
func (r *affinityGroupResource) 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...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
affinityGroupId := model.AffinityGroupId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId)
affinityGroupResp, err := r.client.GetAffinityGroupExecute(ctx, projectId, 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 {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Call API: %v", err))
return
}
err = mapFields(ctx, affinityGroupResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Processing API payload: %v", err))
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Affinity group read")
}
func (r *affinityGroupResource) 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 *affinityGroupResource) 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)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
affinityGroupId := model.AffinityGroupId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId)
// Delete existing affinity group
err := r.client.DeleteAffinityGroupExecute(ctx, projectId, affinityGroupId)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting affinity group", fmt.Sprintf("Calling API: %v", err))
return
}
tflog.Info(ctx, "Affinity group deleted")
}
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] == "" {
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),
)
return
}
projectId := idParts[0]
affinityGroupId := idParts[1]
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId)
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")
}
func toCreatePayload(model *Model) (*iaas.CreateAffinityGroupPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
nameValue := conversion.StringValueToPointer(model.Name)
policyValue := conversion.StringValueToPointer(model.Policy)
return &iaas.CreateAffinityGroupPayload{
Name: nameValue,
Policy: policyValue,
}, nil
}
func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model *Model) error {
if affinityGroupResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("nil model")
}
var affinityGroupId string
if model.AffinityGroupId.ValueString() != "" {
affinityGroupId = model.AffinityGroupId.ValueString()
} else if affinityGroupResp.Id != nil {
affinityGroupId = *affinityGroupResp.Id
} else {
return fmt.Errorf("affinity group id not present")
}
idParts := []string{
model.ProjectId.ValueString(),
affinityGroupId,
}
model.Id = types.StringValue(
strings.Join(idParts, core.Separator),
)
if affinityGroupResp.Members != nil && len(*affinityGroupResp.Members) > 0 {
members, diags := types.ListValueFrom(ctx, types.StringType, *affinityGroupResp.Members)
if diags.HasError() {
return fmt.Errorf("convert members to StringValue list: %w", core.DiagsToError(diags))
}
model.Members = members
} else if model.Members.IsNull() {
model.Members = types.ListNull(types.StringType)
}
model.AffinityGroupId = types.StringValue(affinityGroupId)
model.Name = types.StringPointerValue(affinityGroupResp.Name)
model.Policy = types.StringPointerValue(affinityGroupResp.Policy)
return nil
}

View file

@ -0,0 +1,114 @@
package affinitygroup
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"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) {
tests := []struct {
description string
state Model
input *iaas.AffinityGroup
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
AffinityGroupId: types.StringValue("aid"),
},
&iaas.AffinityGroup{
Id: utils.Ptr("aid"),
},
Model{
Id: types.StringValue("pid,aid"),
ProjectId: types.StringValue("pid"),
AffinityGroupId: types.StringValue("aid"),
Name: types.StringNull(),
Policy: types.StringNull(),
Members: types.ListNull(types.StringType),
},
true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
},
{
"no_affinity_group_id",
Model{
ProjectId: types.StringValue("pid"),
},
&iaas.AffinityGroup{},
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")
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %v", diff)
}
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *iaas.CreateAffinityGroupPayload
isValid bool
}{
{
"default",
&Model{
ProjectId: types.StringValue("pid"),
Name: types.StringValue("name"),
Policy: types.StringValue("policy"),
},
&iaas.CreateAffinityGroupPayload{
Name: utils.Ptr("name"),
Policy: utils.Ptr("policy"),
},
true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toCreatePayload(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)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}

View file

@ -980,6 +980,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo
}
return &iaas.CreateServerPayload{
AffinityGroup: conversion.StringValueToPointer(model.AffinityGroup),
AvailabilityZone: conversion.StringValueToPointer(model.AvailabilityZone),
BootVolume: bootVolumePayload,
ImageId: conversion.StringValueToPointer(model.ImageId),

View file

@ -14,6 +14,7 @@ import (
argusScrapeConfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/argus/scrapeconfig"
dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset"
dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone"
iaasAffinityGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/affinitygroup"
iaasImage "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/image"
iaasKeyPair "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/keypair"
iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network"
@ -413,6 +414,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
argusScrapeConfig.NewScrapeConfigDataSource,
dnsZone.NewZoneDataSource,
dnsRecordSet.NewRecordSetDataSource,
iaasAffinityGroup.NewAffinityGroupDatasource,
iaasImage.NewImageDataSource,
iaasNetwork.NewNetworkDataSource,
iaasNetworkArea.NewNetworkAreaDataSource,
@ -467,6 +469,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
argusScrapeConfig.NewScrapeConfigResource,
dnsZone.NewZoneResource,
dnsRecordSet.NewRecordSetResource,
iaasAffinityGroup.NewAffinityGroupResource,
iaasImage.NewImageResource,
iaasNetwork.NewNetworkResource,
iaasNetworkArea.NewNetworkAreaResource,