IaaS Release (#543)

* IaaS Volume (#541)

* Onboard IaaS Volume

* Labels mapping

* Add acceptance test

* Remove source field

* Fix lint

* Add examples and docs

* Fix lint

* Fix lint

* Fix lint

* Volume source field (#542)

* Onboard IaaS Volume

* Labels mapping

* Add acceptance test

* Remove source field

* Fix lint

* Add examples and docs

* Fix lint

* Fix lint

* Fix lint

* Add source field supoort

* Fix labels and source mapping

* Remove unecessary source mapping

* Move methods to conversion pkg

* Revert change

* Update stackit/internal/services/iaas/volume/datasource.go

Co-authored-by: João Palet <joao.palet@outlook.com>

* Update stackit/internal/services/iaas/volume/resource.go

Co-authored-by: João Palet <joao.palet@outlook.com>

* Update stackit/internal/services/iaas/volume/resource.go

Co-authored-by: João Palet <joao.palet@outlook.com>

* Update stackit/internal/services/iaas/volume/resource.go

Co-authored-by: João Palet <joao.palet@outlook.com>

* Changes after review

* Change after revie

---------

Co-authored-by: João Palet <joao.palet@outlook.com>

* Onboard IaaS security groups (#545)

* onboard iaas security group

* add examples and generate docs

* fix linter issues

* fix deletion

* Update stackit/internal/services/iaas/securitygroup/resource.go

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* rename data source example file

* update docs

* remove field

* remove field

* remove plan modifier from the name field

* refactor labels in mapFields

* change function from utils to conversion

* remove rules from the security group

* update docs

* add security group acceptance test

* add plan modifiers to stateful field

* sort imports

* change stateful description

---------

Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud>
Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* IaaS Server baseline configuration (#546)

* Server resource schema

* Implemente CRUD methods and unit testsg

* Bug fixes

* Bug fix

* Make variable private

* Remove delete_on_termination and update descriptions

* Add security_group field to initial networking

* Add examples and acc test

* Generate docs

* Fix lint

* Fix lint issue

* Fix unit test

* Update desc

* Gen docs

* Onboard IaaS network interface (#544)

* implement network interface

* handle labels

* add CIDR validation

* fix linter issues and generate docs

* remove computed from the allowed addresses and fix the conditions

* Update stackit/internal/services/iaas/networkinterface/resource.go

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* Update stackit/internal/services/iaas/networkinterface/datasource.go

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* apply code review changes

* remove status from schema

* remove unnecessary GET call

* Update stackit/internal/services/iaas/networkinterface/resource.go

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* Update stackit/internal/services/iaas/networkinterface/resource.go

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* rename nic_security to security

* add beta markdown description

* use existing validateIP function

* use utils function for the options listing

* refactor labels

* change function from utils to conversion

* make allowed addresses a list of strings

* add acceptance test for network interfaces

* fix acceptance test

* rename security_groups as security_group_ids

* extend descriptions

* fix acc test

---------

Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud>
Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* rename volume data source example (#552)

Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud>

* add requires replace to ipv4 and ipv6 fields (#549)

Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud>
Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* Server resource improvements (#548)

* Improvements to server resource

* Fix example

* Remove useStateForUnknown

* Update SDK modules

* Update iaasalpha moduel (#555)

* Remove initial networking field (#556)

* Server attachment resources (#557)

* Server attachemnt resources

* Add examples

* Update volume datasource example

* Fix linting issues

* Fix linting

* Fix examples formatting

* Update go.mod

* Revert iaas to v0.11

* Onboard iaas public ip (#551)

* onboard public ip

* onboard public ip

* add public ip acceptance test

* Update examples/data-sources/stackit_public_ip/data-source.tf

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* add plan modifier to IP

* change type in the volume data source

* add network_interface field to public ip resource

* rename network_interface to network_interface_id

* remove obsolete checks

* extend unit tests

* add network_interface_id in example

* extend unit test

* extend acceptance test

* sort imports

---------

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* Add labels to network, network are and network area route resources (#559)

* Fix network_interface example

* Extend network, network area and network area route with labels

* Revert iaas to v0.11.0

---------

Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com>

* Onboard iaas security group rule (#553)

* onboard security group rule

* add security group rule to acceptance test

* change type in examples

* fix acc test issues

* extend example with objects

* remove obsolete field from acceptance test

* remove unnecessary plan modifier

* adapt schema fields

* adapt schema fields

* add requires replace to all fields

* extend descriptions with protocol limitations

* rename subfield protocol to number

* add requires replace to objects

* make icmp_parameters fields required

* add empty field checks for nested objects

* make max and min fields required in the port_range object

* make number field computed in the protocol object

* add UseStateForUnknown in protocol number

* remove obsolete unit test

* add checks for empty protocol and adapt unit test

* add atLeastOneOf validation in protocol fields

* fix linter issues

* Add project existence check before deleting SNA (#561)

* add project list check and error in network area deletion

* Update stackit/internal/services/iaas/networkarea/resource.go

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

---------

Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com>

* Example server use cases and other fixes (#560)

* Add example usage to server resource

* Update examples

* Fix beta warning

* Update docs and examples

* Remove size from example

* Fix server description, fix security group rule error message

* Other fixes

* remove field from datasource

---------

Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com>

* Security group rule fixes (#562)

* Add example usage to server resource

* Update examples

* Fix beta warning

* Update docs and examples

* Remove size from example

* Fix server description, fix security group rule error message

* Other fixes

* Fixes to sec group rule

* Fix lint

* Change after review

---------

Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com>

* Fix server example (#565)

* Fix server example

* Fixes to examples, add CIDR validation to nic

* Migrate iaasalpha to iaas (#568)

* Migrate iaasalpha to iaas

* Fix lint

* Update example

* Improvements to security group rule (#569)

* Improvements to security group rule

* Fix lint

* Fix example and remove computed from description

* Fix formatting

* Update description

---------

Co-authored-by: João Palet <joao.palet@outlook.com>
Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com>
Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud>
This commit is contained in:
Vicente Pinto 2024-10-18 16:37:41 +01:00 committed by GitHub
parent 89dbf777fc
commit 93fe2fe89f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
80 changed files with 10148 additions and 161 deletions

View file

@ -0,0 +1,228 @@
package securitygrouprule
import (
"context"
"fmt"
"net/http"
"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-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"
"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/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// securityGroupRuleDataSourceBetaCheckDone 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 securityGroupRuleDataSourceBetaCheckDone bool
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &securityGroupRuleDataSource{}
)
// NewSecurityGroupRuleDataSource is a helper function to simplify the provider implementation.
func NewSecurityGroupRuleDataSource() datasource.DataSource {
return &securityGroupRuleDataSource{}
}
// securityGroupRuleDataSource is the data source implementation.
type securityGroupRuleDataSource struct {
client *iaas.APIClient
}
// Metadata returns the data source type name.
func (d *securityGroupRuleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_security_group_rule"
}
func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
var apiClient *iaas.APIClient
var err error
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 !securityGroupRuleDataSourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_security_group_rule", "data source")
if resp.Diagnostics.HasError() {
return
}
securityGroupRuleDataSourceBetaCheckDone = true
}
if 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 data source configuration", err))
return
}
d.client = apiClient
tflog.Info(ctx, "iaas client configured")
}
// Schema defines the schema for the resource.
func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
directionOptions := []string{"ingress", "egress"}
resp.Schema = schema.Schema{
MarkdownDescription: features.AddBetaDescription("Security group datasource schema. Must have a `region` specified in the provider configuration."),
Description: "Security group datasource schema. Must have a `region` specified in the provider configuration.",
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`\".",
Computed: true,
},
"project_id": schema.StringAttribute{
Description: "STACKIT project ID to which the security group rule is associated.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"security_group_id": schema.StringAttribute{
Description: "The security group ID.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"security_group_rule_id": schema.StringAttribute{
Description: "The security group rule ID.",
Required: true,
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"direction": schema.StringAttribute{
Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.SupportedValuesDocumentation(directionOptions),
Computed: true,
},
"description": schema.StringAttribute{
Description: "The description of the security group rule.",
Computed: true,
},
"ether_type": schema.StringAttribute{
Description: "The ethertype which the rule should match.",
Computed: true,
},
"icmp_parameters": schema.SingleNestedAttribute{
Description: "ICMP Parameters.",
Computed: true,
Attributes: map[string]schema.Attribute{
"code": schema.Int64Attribute{
Description: "ICMP code. Can be set if the protocol is ICMP.",
Computed: true,
},
"type": schema.Int64Attribute{
Description: "ICMP type. Can be set if the protocol is ICMP.",
Computed: true,
},
},
},
"ip_range": schema.StringAttribute{
Description: "The remote IP range which the rule should match.",
Computed: true,
},
"port_range": schema.SingleNestedAttribute{
Description: "The range of ports.",
Computed: true,
Attributes: map[string]schema.Attribute{
"max": schema.Int64Attribute{
Description: "The maximum port number. Should be greater or equal to the minimum.",
Computed: true,
},
"min": schema.Int64Attribute{
Description: "The minimum port number. Should be less or equal to the minimum.",
Computed: true,
},
},
},
"protocol": schema.SingleNestedAttribute{
Description: "The internet protocol which the rule should match.",
Computed: true,
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The protocol name which the rule should match.",
Computed: true,
},
"number": schema.Int64Attribute{
Description: "The protocol number which the rule should match.",
Computed: true,
},
},
},
"remote_security_group_id": schema.StringAttribute{
Description: "The remote security group which the rule should match.",
Computed: true,
},
},
}
}
// Read refreshes the Terraform state with the latest data.
func (d *securityGroupRuleDataSource) 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()
securityGroupId := model.SecurityGroupId.ValueString()
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
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()
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 security group rule", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFields(securityGroupRuleResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", 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, "security group rule read")
}

View file

@ -0,0 +1,93 @@
package securitygrouprule
import (
"context"
"slices"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
)
// UseNullForUnknownBasedOnProtocolModifier returns a plan modifier that sets a null
// value into the planned value, based on the value of the protocol.name attribute.
//
// To prevent Terraform errors, the framework automatically sets unconfigured
// and Computed attributes to an unknown value "(known after apply)" on update.
// To prevent always showing "(known after apply)" on update for an attribute, e.g. port_range, which never changes in case the protocol is a specific one,
// we set the value to null.
// Examples: port_range is only computed if protocol is not icmp and icmp_parameters is only computed if protocol is icmp
func UseNullForUnknownBasedOnProtocolModifier() planmodifier.Object {
return useNullForUnknownBasedOnProtocolModifier{}
}
// useNullForUnknownBasedOnProtocolModifier implements the plan modifier.
type useNullForUnknownBasedOnProtocolModifier struct{}
func (m useNullForUnknownBasedOnProtocolModifier) Description(_ context.Context) string {
return "If protocol.name attribute is set and the value corresponds to an icmp protocol, the value of this attribute in state will be set to null."
}
// MarkdownDescription returns a markdown description of the plan modifier.
func (m useNullForUnknownBasedOnProtocolModifier) MarkdownDescription(_ context.Context) string {
return "Once set, the value of this attribute in state will be set to null if protocol.name attribute is set and the value corresponds to an icmp protocol."
}
// PlanModifyBool implements the plan modification logic.
func (m useNullForUnknownBasedOnProtocolModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { // nolint:gocritic // function signature required by Terraform
// Check if the resource is being created.
if req.State.Raw.IsNull() {
return
}
// Do nothing if there is a known planned value.
if !req.PlanValue.IsUnknown() {
return
}
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsUnknown() {
return
}
// If there is an unknown configuration value, check if the value of protocol.name attribute corresponds to an icmp protocol. If it does, set the attribute value to null
var model Model
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}
// If protocol is not configured, return without error.
if model.Protocol.IsNull() || model.Protocol.IsUnknown() {
return
}
protocol := &protocolModel{}
diags := model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
protocolName := conversion.StringValueToPointer(protocol.Name)
if protocolName == nil {
return
}
if slices.Contains(icmpProtocols, *protocolName) {
if model.PortRange.IsUnknown() {
resp.PlanValue = types.ObjectNull(portRangeTypes)
return
}
} else {
if model.IcmpParameters.IsUnknown() {
resp.PlanValue = types.ObjectNull(icmpParametersTypes)
return
}
}
// use state for unknown if the value was not set to null
resp.PlanValue = req.StateValue
}

View file

@ -0,0 +1,777 @@
package securitygrouprule
import (
"context"
"fmt"
"net/http"
"slices"
"strings"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"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/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"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-framework/types/basetypes"
"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"
"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/utils"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
)
// resourceBetaCheckDone 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 resourceBetaCheckDone bool
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &securityGroupRuleResource{}
_ resource.ResourceWithConfigure = &securityGroupRuleResource{}
_ resource.ResourceWithImportState = &securityGroupRuleResource{}
icmpProtocols = []string{"icmp", "ipv6-icmp"}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
ProjectId types.String `tfsdk:"project_id"`
SecurityGroupId types.String `tfsdk:"security_group_id"`
SecurityGroupRuleId types.String `tfsdk:"security_group_rule_id"`
Direction types.String `tfsdk:"direction"`
Description types.String `tfsdk:"description"`
EtherType types.String `tfsdk:"ether_type"`
IcmpParameters types.Object `tfsdk:"icmp_parameters"`
IpRange types.String `tfsdk:"ip_range"`
PortRange types.Object `tfsdk:"port_range"`
Protocol types.Object `tfsdk:"protocol"`
RemoteSecurityGroupId types.String `tfsdk:"remote_security_group_id"`
}
type icmpParametersModel struct {
Code types.Int64 `tfsdk:"code"`
Type types.Int64 `tfsdk:"type"`
}
// Types corresponding to icmpParameters
var icmpParametersTypes = map[string]attr.Type{
"code": basetypes.Int64Type{},
"type": basetypes.Int64Type{},
}
type portRangeModel struct {
Max types.Int64 `tfsdk:"max"`
Min types.Int64 `tfsdk:"min"`
}
// Types corresponding to portRange
var portRangeTypes = map[string]attr.Type{
"max": basetypes.Int64Type{},
"min": basetypes.Int64Type{},
}
type protocolModel struct {
Name types.String `tfsdk:"name"`
Number types.Int64 `tfsdk:"number"`
}
// Types corresponding to protocol
var protocolTypes = map[string]attr.Type{
"name": basetypes.StringType{},
"number": basetypes.Int64Type{},
}
// NewSecurityGroupRuleResource is a helper function to simplify the provider implementation.
func NewSecurityGroupRuleResource() resource.Resource {
return &securityGroupRuleResource{}
}
// securityGroupRuleResource is the resource implementation.
type securityGroupRuleResource struct {
client *iaas.APIClient
}
// Metadata returns the resource type name.
func (r *securityGroupRuleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_security_group_rule"
}
// Configure adds the provider configured client to the resource.
func (r *securityGroupRuleResource) 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.ProviderData, got %T", req.ProviderData))
return
}
if !resourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_security_group_rule", "resource")
if resp.Diagnostics.HasError() {
return
}
resourceBetaCheckDone = 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 configuration", err))
return
}
r.client = apiClient
tflog.Info(ctx, "iaas client configured")
}
func (r securityGroupRuleResource) 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() {
return
}
// If protocol is not configured, return without error.
if model.Protocol.IsNull() || model.Protocol.IsUnknown() {
return
}
protocol := &protocolModel{}
diags := model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
protocolName := conversion.StringValueToPointer(protocol.Name)
if protocolName == nil {
return
}
if slices.Contains(icmpProtocols, *protocolName) {
if !(model.PortRange.IsNull() || model.PortRange.IsUnknown()) {
resp.Diagnostics.AddAttributeError(
path.Root("port_range"),
"Conflicting attribute configuration",
"`port_range` attribute can't be provided if `protocol.name` is set to `icmp` or `ipv6-icmp`",
)
}
} else {
if !(model.IcmpParameters.IsNull() || model.IcmpParameters.IsUnknown()) {
resp.Diagnostics.AddAttributeError(
path.Root("icmp_parameters"),
"Conflicting attribute configuration",
"`icmp_parameters` attribute can't be provided if `protocol.name` is not `icmp` or `ipv6-icmp`",
)
}
}
}
// Schema defines the schema for the resource.
func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
directionOptions := []string{"ingress", "egress"}
resp.Schema = schema.Schema{
MarkdownDescription: features.AddBetaDescription("Security group rule resource schema. Must have a `region` specified in the provider configuration."),
Description: "Security group rule 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`,`security_group_id`,`security_group_rule_id`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
},
"project_id": schema.StringAttribute{
Description: "STACKIT project ID to which the security group rule is associated.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"security_group_id": schema.StringAttribute{
Description: "The security group ID.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"security_group_rule_id": schema.StringAttribute{
Description: "The security group rule ID.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
"description": schema.StringAttribute{
Description: "The rule description.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplaceIfConfigured(),
},
Validators: []validator.String{
stringvalidator.LengthAtMost(127),
},
},
"direction": schema.StringAttribute{
Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.SupportedValuesDocumentation(directionOptions),
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"ether_type": schema.StringAttribute{
Description: "The ethertype which the rule should match.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplaceIfConfigured(),
},
},
"icmp_parameters": schema.SingleNestedAttribute{
Description: "ICMP Parameters. These parameters should only be provided if the protocol is ICMP.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
UseNullForUnknownBasedOnProtocolModifier(),
objectplanmodifier.RequiresReplaceIfConfigured(),
},
Attributes: map[string]schema.Attribute{
"code": schema.Int64Attribute{
Description: "ICMP code. Can be set if the protocol is ICMP.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
Validators: []validator.Int64{
int64validator.AtLeast(0),
int64validator.AtMost(255),
},
},
"type": schema.Int64Attribute{
Description: "ICMP type. Can be set if the protocol is ICMP.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
Validators: []validator.Int64{
int64validator.AtLeast(0),
int64validator.AtMost(255),
},
},
},
},
"ip_range": schema.StringAttribute{
Description: "The remote IP range which the rule should match.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.IP(),
},
},
"port_range": schema.SingleNestedAttribute{
Description: "The range of ports. This should only be provided if the protocol is not ICMP.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplaceIfConfigured(),
UseNullForUnknownBasedOnProtocolModifier(),
},
Attributes: map[string]schema.Attribute{
"max": schema.Int64Attribute{
Description: "The maximum port number. Should be greater or equal to the minimum.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
Validators: []validator.Int64{
int64validator.AtLeast(0),
int64validator.AtMost(65535),
},
},
"min": schema.Int64Attribute{
Description: "The minimum port number. Should be less or equal to the maximum.",
Required: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.RequiresReplace(),
},
Validators: []validator.Int64{
int64validator.AtLeast(0),
int64validator.AtMost(65535),
},
},
},
},
"protocol": schema.SingleNestedAttribute{
Description: "The internet protocol which the rule should match.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplaceIfConfigured(),
objectplanmodifier.UseStateForUnknown(),
},
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Description: "The protocol name which the rule should match. Either `name` or `number` must be provided.",
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.AtLeastOneOf(
path.MatchRoot("protocol").AtName("number"),
),
stringvalidator.ConflictsWith(
path.MatchRoot("protocol").AtName("number"),
),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
stringplanmodifier.RequiresReplaceIfConfigured(),
},
},
"number": schema.Int64Attribute{
Description: "The protocol number which the rule should match. Either `name` or `number` must be provided.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
int64planmodifier.RequiresReplaceIfConfigured(),
},
Validators: []validator.Int64{
int64validator.AtLeast(0),
int64validator.AtMost(255),
},
},
},
},
"remote_security_group_id": schema.StringAttribute{
Description: "The remote security group which the rule should match.",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
validate.UUID(),
validate.NoSeparator(),
},
},
},
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *securityGroupRuleResource) 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...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
securityGroupId := model.SecurityGroupId.ValueString()
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
var icmpParameters *icmpParametersModel
if !(model.IcmpParameters.IsNull() || model.IcmpParameters.IsUnknown()) {
icmpParameters = &icmpParametersModel{}
diags = model.IcmpParameters.As(ctx, icmpParameters, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
var portRange *portRangeModel
if !(model.PortRange.IsNull() || model.PortRange.IsUnknown()) {
portRange = &portRangeModel{}
diags = model.PortRange.As(ctx, portRange, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
var protocol *protocolModel
if !(model.Protocol.IsNull() || model.Protocol.IsUnknown()) {
protocol = &protocolModel{}
diags = model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{})
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Generate API request body from model
payload, err := toCreatePayload(&model, icmpParameters, portRange, protocol)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create new security group rule
securityGroupRule, err := r.client.CreateSecurityGroupRule(ctx, projectId, securityGroupId).CreateSecurityGroupRulePayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Calling API: %v", err))
return
}
ctx = tflog.SetField(ctx, "security_group_rule_id", *securityGroupRule.Id)
// Map response body to schema
err = mapFields(securityGroupRule, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", 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, "Security group rule created")
}
// Read refreshes the Terraform state with the latest data.
func (r *securityGroupRuleResource) 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()
securityGroupId := model.SecurityGroupId.ValueString()
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
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()
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 security group rule", fmt.Sprintf("Calling API: %v", err))
return
}
// Map response body to schema
err = mapFields(securityGroupRuleResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", 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, "security group rule read")
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *securityGroupRuleResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Update shouldn't be called
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group rule", "Security group rule can't be updated")
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *securityGroupRuleResource) 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()
securityGroupId := model.SecurityGroupId.ValueString()
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
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()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group rule", fmt.Sprintf("Calling API: %v", err))
return
}
tflog.Info(ctx, "security group rule deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,security_group_id, security_group_rule_id
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] == "" {
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),
)
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)
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 {
if securityGroupRuleResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var securityGroupRuleId string
if model.SecurityGroupRuleId.ValueString() != "" {
securityGroupRuleId = model.SecurityGroupRuleId.ValueString()
} else if securityGroupRuleResp.Id != nil {
securityGroupRuleId = *securityGroupRuleResp.Id
} else {
return fmt.Errorf("security group rule id not present")
}
idParts := []string{
model.ProjectId.ValueString(),
model.SecurityGroupId.ValueString(),
securityGroupRuleId,
}
model.Id = types.StringValue(
strings.Join(idParts, core.Separator),
)
model.SecurityGroupRuleId = types.StringValue(securityGroupRuleId)
model.Direction = types.StringPointerValue(securityGroupRuleResp.Direction)
model.Description = types.StringPointerValue(securityGroupRuleResp.Description)
model.EtherType = types.StringPointerValue(securityGroupRuleResp.Ethertype)
model.IpRange = types.StringPointerValue(securityGroupRuleResp.IpRange)
model.RemoteSecurityGroupId = types.StringPointerValue(securityGroupRuleResp.RemoteSecurityGroupId)
err := mapIcmpParameters(securityGroupRuleResp, model)
if err != nil {
return fmt.Errorf("map icmp_parameters: %w", err)
}
err = mapPortRange(securityGroupRuleResp, model)
if err != nil {
return fmt.Errorf("map port_range: %w", err)
}
err = mapProtocol(securityGroupRuleResp, model)
if err != nil {
return fmt.Errorf("map protocol: %w", err)
}
return nil
}
func mapIcmpParameters(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error {
if securityGroupRuleResp.IcmpParameters == nil {
m.IcmpParameters = types.ObjectNull(icmpParametersTypes)
return nil
}
icmpParametersValues := map[string]attr.Value{
"type": types.Int64Value(*securityGroupRuleResp.IcmpParameters.Type),
"code": types.Int64Value(*securityGroupRuleResp.IcmpParameters.Code),
}
icmpParametersObject, diags := types.ObjectValue(icmpParametersTypes, icmpParametersValues)
if diags.HasError() {
return fmt.Errorf("create icmpParameters object: %w", core.DiagsToError(diags))
}
m.IcmpParameters = icmpParametersObject
return nil
}
func mapPortRange(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error {
if securityGroupRuleResp.PortRange == nil {
m.PortRange = types.ObjectNull(portRangeTypes)
return nil
}
portRangeMax := types.Int64Null()
portRangeMin := types.Int64Null()
if securityGroupRuleResp.PortRange.Max != nil {
portRangeMax = types.Int64Value(*securityGroupRuleResp.PortRange.Max)
}
if securityGroupRuleResp.PortRange.Min != nil {
portRangeMin = types.Int64Value(*securityGroupRuleResp.PortRange.Min)
}
portRangeValues := map[string]attr.Value{
"max": portRangeMax,
"min": portRangeMin,
}
portRangeObject, diags := types.ObjectValue(portRangeTypes, portRangeValues)
if diags.HasError() {
return fmt.Errorf("create portRange object: %w", core.DiagsToError(diags))
}
m.PortRange = portRangeObject
return nil
}
func mapProtocol(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error {
if securityGroupRuleResp.Protocol == nil {
m.Protocol = types.ObjectNull(protocolTypes)
return nil
}
protocolNumberValue := types.Int64Null()
if securityGroupRuleResp.Protocol.Number != nil {
protocolNumberValue = types.Int64Value(*securityGroupRuleResp.Protocol.Number)
}
protocolNameValue := types.StringNull()
if securityGroupRuleResp.Protocol.Name != nil {
protocolNameValue = types.StringValue(*securityGroupRuleResp.Protocol.Name)
}
protocolValues := map[string]attr.Value{
"name": protocolNameValue,
"number": protocolNumberValue,
}
protocolObject, diags := types.ObjectValue(protocolTypes, protocolValues)
if diags.HasError() {
return fmt.Errorf("create protocol object: %w", core.DiagsToError(diags))
}
m.Protocol = protocolObject
return nil
}
func toCreatePayload(model *Model, icmpParameters *icmpParametersModel, portRange *portRangeModel, protocol *protocolModel) (*iaas.CreateSecurityGroupRulePayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
payloadIcmpParameters, err := toIcmpParametersPayload(icmpParameters)
if err != nil {
return nil, fmt.Errorf("converting icmp parameters: %w", err)
}
payloadPortRange, err := toPortRangePayload(portRange)
if err != nil {
return nil, fmt.Errorf("converting port range: %w", err)
}
payloadProtocol, err := toProtocolPayload(protocol)
if err != nil {
return nil, fmt.Errorf("converting protocol: %w", err)
}
return &iaas.CreateSecurityGroupRulePayload{
Description: conversion.StringValueToPointer(model.Description),
Direction: conversion.StringValueToPointer(model.Direction),
Ethertype: conversion.StringValueToPointer(model.EtherType),
IpRange: conversion.StringValueToPointer(model.IpRange),
RemoteSecurityGroupId: conversion.StringValueToPointer(model.RemoteSecurityGroupId),
IcmpParameters: payloadIcmpParameters,
PortRange: payloadPortRange,
Protocol: payloadProtocol,
}, nil
}
func toIcmpParametersPayload(icmpParameters *icmpParametersModel) (*iaas.ICMPParameters, error) {
if icmpParameters == nil {
return nil, nil
}
payloadParams := &iaas.ICMPParameters{}
payloadParams.Code = conversion.Int64ValueToPointer(icmpParameters.Code)
payloadParams.Type = conversion.Int64ValueToPointer(icmpParameters.Type)
return payloadParams, nil
}
func toPortRangePayload(portRange *portRangeModel) (*iaas.PortRange, error) {
if portRange == nil {
return nil, nil
}
payloadPortRange := &iaas.PortRange{}
payloadPortRange.Max = conversion.Int64ValueToPointer(portRange.Max)
payloadPortRange.Min = conversion.Int64ValueToPointer(portRange.Min)
return payloadPortRange, nil
}
func toProtocolPayload(protocol *protocolModel) (*iaas.CreateProtocol, error) {
if protocol == nil {
return nil, nil
}
payloadProtocol := &iaas.CreateProtocol{}
payloadProtocol.String = conversion.StringValueToPointer(protocol.Name)
payloadProtocol.Int64 = conversion.Int64ValueToPointer(protocol.Number)
return payloadProtocol, nil
}

View file

@ -0,0 +1,305 @@
package securitygrouprule
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/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
var fixtureModelIcmpParameters = types.ObjectValueMust(icmpParametersTypes, map[string]attr.Value{
"code": types.Int64Value(1),
"type": types.Int64Value(2),
})
var fixtureIcmpParameters = iaas.ICMPParameters{
Code: utils.Ptr(int64(1)),
Type: utils.Ptr(int64(2)),
}
var fixtureModelPortRange = types.ObjectValueMust(portRangeTypes, map[string]attr.Value{
"max": types.Int64Value(2),
"min": types.Int64Value(1),
})
var fixturePortRange = iaas.PortRange{
Max: utils.Ptr(int64(2)),
Min: utils.Ptr(int64(1)),
}
var fixtureModelProtocol = types.ObjectValueMust(protocolTypes, map[string]attr.Value{
"name": types.StringValue("name"),
"number": types.Int64Value(1),
})
var fixtureProtocol = iaas.Protocol{
Name: utils.Ptr("name"),
Number: utils.Ptr(int64(1)),
}
var fixtureModelCreateProtocol = types.ObjectValueMust(protocolTypes, map[string]attr.Value{
"name": types.StringValue("name"),
"number": types.Int64Null(),
})
var fixtureCreateProtocol = iaas.CreateProtocol{
String: utils.Ptr("name"),
}
func TestMapFields(t *testing.T) {
tests := []struct {
description string
state Model
input *iaas.SecurityGroupRule
expected Model
isValid bool
}{
{
"default_values",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
},
Model{
Id: types.StringValue("pid,sgid,sgrid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Direction: types.StringNull(),
Description: types.StringNull(),
EtherType: types.StringNull(),
IpRange: types.StringNull(),
RemoteSecurityGroupId: types.StringNull(),
IcmpParameters: types.ObjectNull(icmpParametersTypes),
PortRange: types.ObjectNull(portRangeTypes),
Protocol: types.ObjectNull(protocolTypes),
},
true,
},
{
"simple_values",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
},
&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"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Direction: types.StringValue("ingress"),
Description: types.StringValue("desc"),
EtherType: types.StringValue("ether"),
IpRange: types.StringValue("iprange"),
RemoteSecurityGroupId: types.StringValue("remote"),
IcmpParameters: fixtureModelIcmpParameters,
PortRange: fixtureModelPortRange,
Protocol: fixtureModelProtocol,
},
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(),
}),
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Protocol: &fixtureProtocol,
},
Model{
Id: types.StringValue("pid,sgid,sgrid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Direction: types.StringNull(),
Description: types.StringNull(),
EtherType: types.StringNull(),
IpRange: types.StringNull(),
RemoteSecurityGroupId: types.StringNull(),
IcmpParameters: types.ObjectNull(icmpParametersTypes),
PortRange: types.ObjectNull(portRangeTypes),
Protocol: fixtureModelProtocol,
},
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),
}),
},
&iaas.SecurityGroupRule{
Id: utils.Ptr("sgrid"),
Protocol: &fixtureProtocol,
},
Model{
Id: types.StringValue("pid,sgid,sgrid"),
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
SecurityGroupRuleId: types.StringValue("sgrid"),
Direction: types.StringNull(),
Description: types.StringNull(),
EtherType: types.StringNull(),
IpRange: types.StringNull(),
RemoteSecurityGroupId: types.StringNull(),
IcmpParameters: types.ObjectNull(icmpParametersTypes),
PortRange: types.ObjectNull(portRangeTypes),
Protocol: fixtureModelProtocol,
},
true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
},
{
"no_resource_id",
Model{
ProjectId: types.StringValue("pid"),
SecurityGroupId: types.StringValue("sgid"),
},
&iaas.SecurityGroupRule{},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(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
expected *iaas.CreateSecurityGroupRulePayload
isValid bool
}{
{
"default_values",
&Model{},
&iaas.CreateSecurityGroupRulePayload{},
true,
},
{
"default_ok",
&Model{
Description: types.StringValue("desc"),
Direction: types.StringValue("ingress"),
IcmpParameters: fixtureModelIcmpParameters,
PortRange: fixtureModelPortRange,
Protocol: fixtureModelCreateProtocol,
},
&iaas.CreateSecurityGroupRulePayload{
Description: utils.Ptr("desc"),
Direction: utils.Ptr("ingress"),
IcmpParameters: &fixtureIcmpParameters,
PortRange: &fixturePortRange,
Protocol: &fixtureCreateProtocol,
},
true,
},
{
"nil_model",
nil,
nil,
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
var icmpParameters *icmpParametersModel
var portRange *portRangeModel
var protocol *protocolModel
if tt.input != nil {
if !(tt.input.IcmpParameters.IsNull() || tt.input.IcmpParameters.IsUnknown()) {
icmpParameters = &icmpParametersModel{}
diags := tt.input.IcmpParameters.As(context.Background(), icmpParameters, basetypes.ObjectAsOptions{})
if diags.HasError() {
t.Fatalf("Error converting icmp parameters: %v", diags.Errors())
}
}
if !(tt.input.PortRange.IsNull() || tt.input.PortRange.IsUnknown()) {
portRange = &portRangeModel{}
diags := tt.input.PortRange.As(context.Background(), portRange, basetypes.ObjectAsOptions{})
if diags.HasError() {
t.Fatalf("Error converting port range: %v", diags.Errors())
}
}
if !(tt.input.Protocol.IsNull() || tt.input.Protocol.IsUnknown()) {
protocol = &protocolModel{}
diags := tt.input.Protocol.As(context.Background(), protocol, basetypes.ObjectAsOptions{})
if diags.HasError() {
t.Fatalf("Error converting protocol: %v", diags.Errors())
}
}
}
output, err := toCreatePayload(tt.input, icmpParameters, portRange, protocol)
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)
}
}
})
}
}