Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 77 additions & 35 deletions internal/services/instance/security_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
instanceSDK "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/identity"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/zonal"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/services/account"
Expand All @@ -31,6 +32,7 @@ func ResourceSecurityGroup() *schema.Resource {
Default: schema.DefaultTimeout(defaultInstanceSecurityGroupTimeout),
},
SchemaFunc: securityGroupSchema,
Identity: identity.DefaultZonal(),
}
}

Expand Down Expand Up @@ -135,15 +137,43 @@ func ResourceInstanceSecurityGroupCreate(ctx context.Context, d *schema.Resource
return diag.FromErr(err)
}

d.SetId(zonal.NewIDString(zone, res.SecurityGroup.ID))
err = identity.SetZonalIdentity(d, res.SecurityGroup.Zone, res.SecurityGroup.ID)
if err != nil {
return diag.FromErr(err)
}

if d.Get("external_rules").(bool) {
return ResourceInstanceSecurityGroupRead(ctx, d, m)
return setSecurityGroupState(ctx, instanceAPI, d, res.SecurityGroup)
}
// We call update instead of read as it will take care of creating rules.
return ResourceInstanceSecurityGroupUpdate(ctx, d, m)
}

func setSecurityGroupState(ctx context.Context, instanceAPI *instanceSDK.API, d *schema.ResourceData, sg *instanceSDK.SecurityGroup) diag.Diagnostics {
_ = d.Set("zone", sg.Zone)
_ = d.Set("organization_id", sg.Organization)
_ = d.Set("project_id", sg.Project)
_ = d.Set("name", sg.Name)
_ = d.Set("stateful", sg.Stateful)
_ = d.Set("description", sg.Description)
_ = d.Set("inbound_default_policy", sg.InboundDefaultPolicy.String())
_ = d.Set("outbound_default_policy", sg.OutboundDefaultPolicy.String())
_ = d.Set("enable_default_security", sg.EnableDefaultSecurity)
_ = d.Set("tags", sg.Tags)

if !d.Get("external_rules").(bool) {
inboundRules, outboundRules, err := getSecurityGroupRules(ctx, instanceAPI, sg.Zone, sg.ID, d)
if err != nil {
return diag.FromErr(err)
}

_ = d.Set("inbound_rule", inboundRules)
_ = d.Set("outbound_rule", outboundRules)
}

return nil
}

func ResourceInstanceSecurityGroupRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
instanceAPI, zone, ID, err := NewAPIWithZoneAndID(m, d.Id())
if err != nil {
Expand All @@ -164,28 +194,12 @@ func ResourceInstanceSecurityGroupRead(ctx context.Context, d *schema.ResourceDa
return diag.FromErr(err)
}

_ = d.Set("zone", zone)
_ = d.Set("organization_id", res.SecurityGroup.Organization)
_ = d.Set("project_id", res.SecurityGroup.Project)
_ = d.Set("name", res.SecurityGroup.Name)
_ = d.Set("stateful", res.SecurityGroup.Stateful)
_ = d.Set("description", res.SecurityGroup.Description)
_ = d.Set("inbound_default_policy", res.SecurityGroup.InboundDefaultPolicy.String())
_ = d.Set("outbound_default_policy", res.SecurityGroup.OutboundDefaultPolicy.String())
_ = d.Set("enable_default_security", res.SecurityGroup.EnableDefaultSecurity)
_ = d.Set("tags", res.SecurityGroup.Tags)

if !d.Get("external_rules").(bool) {
inboundRules, outboundRules, err := getSecurityGroupRules(ctx, instanceAPI, zone, ID, d)
if err != nil {
return diag.FromErr(err)
}

_ = d.Set("inbound_rule", inboundRules)
_ = d.Set("outbound_rule", outboundRules)
err = identity.SetZonalIdentity(d, res.SecurityGroup.Zone, res.SecurityGroup.ID)
if err != nil {
return diag.FromErr(err)
}

return nil
return setSecurityGroupState(ctx, instanceAPI, d, res.SecurityGroup)
}

func getSecurityGroupRules(ctx context.Context, instanceAPI *instanceSDK.API, zone scw.Zone, securityGroupID string, d *schema.ResourceData) ([]any, []any, error) {
Expand Down Expand Up @@ -223,19 +237,28 @@ func getSecurityGroupRules(ctx context.Context, instanceAPI *instanceSDK.API, zo
for direction := range apiRules {
for index, apiRule := range apiRules[direction] {
if index < len(stateRules[direction]) {
stateRuleRaw := stateRules[direction][index].(map[string]any)

var ipFieldToSet string
if _, ok := stateRuleRaw["ip"]; ok {
ipFieldToSet = "ip"
} else if _, ok := stateRuleRaw["ip_range"]; ok {
ipFieldToSet = "ip_range"
}

stateRule, errGroup := securityGroupRuleExpand(stateRules[direction][index])
if errGroup != nil {
return nil, nil, errGroup
}

if ok, _ := SecurityGroupRuleEquals(stateRule, apiRule); !ok {
stateRules[direction][index], err = securityGroupRuleFlatten(apiRule)
stateRules[direction][index], err = securityGroupRuleFlatten(apiRule, ipFieldToSet)
if err != nil {
return nil, nil, err
}
}
} else {
rulesGroup, err := securityGroupRuleFlatten(apiRule)
rulesGroup, err := securityGroupRuleFlatten(apiRule, "")
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -303,7 +326,7 @@ func ResourceInstanceSecurityGroupUpdate(ctx context.Context, d *schema.Resource
updateReq.Name = types.ExpandStringPtr(d.Get("name"))
}

_, err = instanceAPI.UpdateSecurityGroup(updateReq, scw.WithContext(ctx))
res, err := instanceAPI.UpdateSecurityGroup(updateReq, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}
Expand All @@ -315,7 +338,7 @@ func ResourceInstanceSecurityGroupUpdate(ctx context.Context, d *schema.Resource
}
}

return ResourceInstanceSecurityGroupRead(ctx, d, m)
return setSecurityGroupState(ctx, instanceAPI, d, res.SecurityGroup)
}

// updateSecurityGroupeRules handles updating SecurityGroupRules
Expand Down Expand Up @@ -479,7 +502,13 @@ func securityGroupRuleExpand(i any) (*instanceSDK.SecurityGroupRule, error) {
}

// securityGroupRuleFlatten transform an api rule to a state one.
func securityGroupRuleFlatten(rule *instanceSDK.SecurityGroupRule) (map[string]any, error) {
func securityGroupRuleFlatten(rule *instanceSDK.SecurityGroupRule, ipFieldToSet string) (map[string]any, error) {
res := map[string]any{
"protocol": rule.Protocol.String(),
"action": rule.Action.String(),
}

// Set port or port_range
portFrom, portTo := uint32(0), uint32(0)

if rule.DestPortFrom != nil {
Expand All @@ -490,16 +519,29 @@ func securityGroupRuleFlatten(rule *instanceSDK.SecurityGroupRule) (map[string]a
portTo = *rule.DestPortTo
}

ipnetRange, err := types.FlattenIPNet(rule.IPRange)
if err != nil {
return nil, err
if portFrom != 0 {
if portTo != 0 {
res["port_range"] = fmt.Sprintf("%d-%d", portFrom, portTo)
} else {
res["port"] = portFrom
}
}

res := map[string]any{
"protocol": rule.Protocol.String(),
"ip_range": ipnetRange,
"port_range": fmt.Sprintf("%d-%d", portFrom, portTo),
"action": rule.Action.String(),
// Set ip or ip_range
switch ipFieldToSet {
case "ip":
res["ip"] = rule.IPRange.IP.String()
case "ip_range":
res["ip_range"] = rule.IPRange.String()
case "":
// If we don't have access to the IP field that was set in the config (most likely in an import context),
// we always set 'ip_range' with the value from the API, and 'ip' if the value is not a range.
res["ip_range"] = rule.IPRange.String()
if one, _ := rule.IPRange.Mask.Size(); one == 32 {
res["ip"] = rule.IPRange.IP.String()
}
default:
return nil, fmt.Errorf("unknown IP field to set: %q", ipFieldToSet)
}

return res, nil
Expand Down
21 changes: 20 additions & 1 deletion internal/services/instance/security_group_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/datasource"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/types"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/verify"
)
Expand Down Expand Up @@ -41,6 +42,8 @@ func DataSourceInstanceSecurityGroupRead(ctx context.Context, d *schema.Resource
return diag.FromErr(err)
}

var securityGroup *instance.SecurityGroup

securityGroupID, ok := d.GetOk("security_group_id")
if !ok {
sgName := d.Get("name").(string)
Expand All @@ -63,12 +66,28 @@ func DataSourceInstanceSecurityGroupRead(ctx context.Context, d *schema.Resource
return diag.FromErr(err)
}

securityGroup = foundSG
securityGroupID = foundSG.ID
} else {
id, err := locality.ExtractUUID(securityGroupID.(string))
if err != nil {
return diag.FromErr(err)
}

res, err := instanceAPI.GetSecurityGroup(&instance.GetSecurityGroupRequest{
Zone: zone,
SecurityGroupID: id,
}, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

securityGroup = res.SecurityGroup
}

zonedID := datasource.NewZonedID(securityGroupID, zone)
d.SetId(zonedID)
_ = d.Set("security_group_id", zonedID)

return ResourceInstanceSecurityGroupRead(ctx, d, m)
return setSecurityGroupState(ctx, instanceAPI, d, securityGroup)
}
69 changes: 55 additions & 14 deletions internal/services/instance/security_group_rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import (

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/identity"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/zonal"
"github.com/scaleway/terraform-provider-scaleway/v2/internal/verify"
)

func ResourceSecurityGroupRules() *schema.Resource {
Expand All @@ -21,6 +27,7 @@ func ResourceSecurityGroupRules() *schema.Resource {
Default: schema.DefaultTimeout(defaultInstanceSecurityGroupRuleTimeout),
},
SchemaFunc: securityGroupRulesSchema,
Identity: identity.DefaultZonal(),
}
}

Expand All @@ -31,8 +38,9 @@ func securityGroupRulesSchema() map[string]*schema.Schema {
Required: true,
// Ensure SecurityGroupRules.ID and SecurityGroupRules.security_group_id stay in sync.
// If security_group_id is changed, a new SecurityGroupRules is created, with a new ID.
ForceNew: true,
Description: "The security group associated with this volume",
ForceNew: true,
Description: "The security group associated with this volume",
ValidateDiagFunc: verify.IsUUIDWithLocality(),
},
"inbound_rule": {
Type: schema.TypeList,
Expand All @@ -50,23 +58,24 @@ func securityGroupRulesSchema() map[string]*schema.Schema {
}

func ResourceInstanceSecurityGroupRulesCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
d.SetId(d.Get("security_group_id").(string))

// We call update instead of read as it will take care of creating rules.
return ResourceInstanceSecurityGroupRulesUpdate(ctx, d, m)
}

func ResourceInstanceSecurityGroupRulesRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
securityGroupZonedID := d.Id()
zone, securityGroupID, err := locality.ParseLocalizedID(d.Get("security_group_id").(string))
if err != nil {
return diag.FromErr(err)
}

instanceAPI, zone, securityGroupID, err := NewAPIWithZoneAndID(m, securityGroupZonedID)
err = identity.SetZonalIdentity(d, scw.Zone(zone), securityGroupID)
if err != nil {
return diag.FromErr(err)
}

_ = d.Set("security_group_id", securityGroupZonedID)
// We call update instead of read as it will take care of creating rules.
return ResourceInstanceSecurityGroupRulesUpdate(ctx, d, m)
}

func setSecurityGroupRulesState(ctx context.Context, d *schema.ResourceData, instanceAPI *instance.API, sg *instance.SecurityGroup) diag.Diagnostics {
_ = d.Set("security_group_id", zonal.NewID(sg.Zone, sg.ID).String())

inboundRules, outboundRules, err := getSecurityGroupRules(ctx, instanceAPI, zone, securityGroupID, d)
inboundRules, outboundRules, err := getSecurityGroupRules(ctx, instanceAPI, sg.Zone, sg.ID, d)
if err != nil {
if httperrors.Is404(err) {
d.SetId("")
Expand All @@ -83,6 +92,30 @@ func ResourceInstanceSecurityGroupRulesRead(ctx context.Context, d *schema.Resou
return nil
}

func ResourceInstanceSecurityGroupRulesRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
securityGroupZonedID := d.Id()

instanceAPI, zone, securityGroupID, err := NewAPIWithZoneAndID(m, securityGroupZonedID)
if err != nil {
return diag.FromErr(err)
}

sg, err := instanceAPI.GetSecurityGroup(&instance.GetSecurityGroupRequest{
Zone: zone,
SecurityGroupID: securityGroupID,
}, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

err = identity.SetZonalIdentity(d, sg.SecurityGroup.Zone, sg.SecurityGroup.ID)
if err != nil {
return diag.FromErr(err)
}

return setSecurityGroupRulesState(ctx, d, instanceAPI, sg.SecurityGroup)
}

func ResourceInstanceSecurityGroupRulesUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
securityGroupZonedID := d.Id()

Expand All @@ -96,7 +129,15 @@ func ResourceInstanceSecurityGroupRulesUpdate(ctx context.Context, d *schema.Res
return diag.FromErr(err)
}

return ResourceInstanceSecurityGroupRulesRead(ctx, d, m)
sg, err := instanceAPI.GetSecurityGroup(&instance.GetSecurityGroupRequest{
Zone: zone,
SecurityGroupID: securityGroupID,
}, scw.WithContext(ctx))
if err != nil {
return diag.FromErr(err)
}

return setSecurityGroupRulesState(ctx, d, instanceAPI, sg.SecurityGroup)
}

func ResourceInstanceSecurityGroupRulesDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
Expand Down
Loading
Loading