Skip to content

Commit 41ef848

Browse files
committed
feat(k8s): ACL CRUD + basic test
1 parent 4e9a3d5 commit 41ef848

File tree

4 files changed

+3802
-0
lines changed

4 files changed

+3802
-0
lines changed

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ func Provider(config *Config) plugin.ProviderFunc {
185185
"scaleway_ipam_ip": ipam.ResourceIP(),
186186
"scaleway_ipam_ip_reverse_dns": ipam.ResourceIPReverseDNS(),
187187
"scaleway_job_definition": jobs.ResourceDefinition(),
188+
"scaleway_k8s_acl": k8s.ResourceACL(),
188189
"scaleway_k8s_cluster": k8s.ResourceCluster(),
189190
"scaleway_k8s_pool": k8s.ResourcePool(),
190191
"scaleway_lb": lb.ResourceLb(),

internal/services/k8s/acl.go

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
package k8s
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
9+
"github.com/scaleway/scaleway-sdk-go/api/k8s/v1"
10+
"github.com/scaleway/scaleway-sdk-go/scw"
11+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/cdf"
12+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/dsf"
13+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors"
14+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality"
15+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional"
16+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/types"
17+
"github.com/scaleway/terraform-provider-scaleway/v2/internal/verify"
18+
)
19+
20+
func ResourceACL() *schema.Resource {
21+
return &schema.Resource{
22+
CreateContext: ResourceACLCreate,
23+
ReadContext: ResourceACLRead,
24+
UpdateContext: ResourceACLUpdate,
25+
DeleteContext: ResourceACLDelete,
26+
Importer: &schema.ResourceImporter{
27+
StateContext: schema.ImportStatePassthroughContext,
28+
},
29+
Timeouts: &schema.ResourceTimeout{
30+
Create: schema.DefaultTimeout(defaultK8SClusterTimeout),
31+
Read: schema.DefaultTimeout(defaultK8SClusterTimeout),
32+
Update: schema.DefaultTimeout(defaultK8SClusterTimeout),
33+
Delete: schema.DefaultTimeout(defaultK8SClusterTimeout),
34+
Default: schema.DefaultTimeout(defaultK8SClusterTimeout),
35+
},
36+
SchemaVersion: 0,
37+
Schema: map[string]*schema.Schema{
38+
"cluster_id": {
39+
Type: schema.TypeString,
40+
Required: true,
41+
ForceNew: true,
42+
ValidateDiagFunc: verify.IsUUIDorUUIDWithLocality(),
43+
DiffSuppressFunc: dsf.Locality,
44+
Description: "Cluster on which the ACL is applied",
45+
},
46+
"acl_rules": {
47+
Type: schema.TypeList,
48+
Required: true,
49+
Description: "The list of network rules that manage inbound traffic",
50+
Elem: &schema.Resource{
51+
Schema: map[string]*schema.Schema{
52+
"ip": {
53+
Type: schema.TypeString,
54+
Optional: true,
55+
Description: "The IP subnet to be allowed",
56+
ValidateFunc: validation.IsCIDR,
57+
},
58+
"scaleway_ranges": {
59+
Type: schema.TypeBool,
60+
Optional: true,
61+
Description: "Allow access to cluster from all Scaleway ranges as defined in https://www.scaleway.com/en/docs/console/account/reference-content/scaleway-network-information/#ip-ranges-used-by-scaleway. Only one rule with this field set to true can be added",
62+
},
63+
"description": {
64+
Type: schema.TypeString,
65+
Optional: true,
66+
Description: "The description of the ACL rule",
67+
},
68+
"id": {
69+
Type: schema.TypeString,
70+
Computed: true,
71+
Description: "The ID of the ACL rule",
72+
},
73+
},
74+
},
75+
},
76+
// Common
77+
"region": regional.Schema(),
78+
},
79+
CustomizeDiff: cdf.LocalityCheck("cluster_id"),
80+
}
81+
}
82+
83+
func ResourceACLCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
84+
api, _, err := newAPIWithRegion(d, m)
85+
if err != nil {
86+
return diag.FromErr(err)
87+
}
88+
89+
region, clusterID, err := regional.ParseID(d.Get("cluster_id").(string))
90+
if err != nil {
91+
return diag.FromErr(err)
92+
}
93+
94+
_, err = waitCluster(ctx, api, region, locality.ExpandID(clusterID), d.Timeout(schema.TimeoutCreate))
95+
if err != nil {
96+
return diag.FromErr(err)
97+
}
98+
99+
acls, err := expandACL(d.Get("acl_rules").([]interface{}))
100+
if err != nil {
101+
return diag.FromErr(err)
102+
}
103+
104+
createReq := &k8s.SetClusterACLRulesRequest{
105+
Region: region,
106+
ClusterID: clusterID,
107+
ACLs: acls,
108+
}
109+
110+
_, err = api.SetClusterACLRules(createReq, scw.WithContext(ctx))
111+
if err != nil {
112+
return diag.FromErr(err)
113+
}
114+
115+
regionalID := regional.NewID(region, clusterID).String()
116+
d.SetId(regionalID)
117+
118+
return ResourceACLRead(ctx, d, m)
119+
}
120+
121+
func ResourceACLRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
122+
api, region, clusterID, err := NewAPIWithRegionAndID(m, d.Id())
123+
if err != nil {
124+
return diag.FromErr(err)
125+
}
126+
127+
_, err = waitCluster(ctx, api, region, clusterID, d.Timeout(schema.TimeoutRead))
128+
if err != nil && !httperrors.Is404(err) {
129+
return diag.FromErr(err)
130+
}
131+
132+
acls, err := api.ListClusterACLRules(&k8s.ListClusterACLRulesRequest{
133+
Region: region,
134+
ClusterID: clusterID,
135+
}, scw.WithContext(ctx))
136+
if err != nil {
137+
if httperrors.Is404(err) {
138+
d.SetId("")
139+
140+
return nil
141+
}
142+
143+
return diag.FromErr(err)
144+
}
145+
146+
_ = d.Set("cluster_id", clusterID)
147+
_ = d.Set("acl_rules", flattenACL(acls.Rules))
148+
149+
return nil
150+
}
151+
152+
func ResourceACLUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
153+
api, region, clusterID, err := NewAPIWithRegionAndID(m, d.Id())
154+
if err != nil {
155+
return diag.FromErr(err)
156+
}
157+
158+
_, err = waitCluster(ctx, api, region, clusterID, d.Timeout(schema.TimeoutUpdate))
159+
if err != nil && !httperrors.Is404(err) {
160+
return diag.FromErr(err)
161+
}
162+
163+
if d.HasChange("acl_rules") {
164+
acls, err := expandACL(d.Get("acl_rules").([]interface{}))
165+
if err != nil {
166+
return diag.FromErr(err)
167+
}
168+
169+
req := &k8s.SetClusterACLRulesRequest{
170+
Region: region,
171+
ClusterID: clusterID,
172+
ACLs: acls,
173+
}
174+
175+
_, err = api.SetClusterACLRules(req, scw.WithContext(ctx))
176+
if err != nil {
177+
return diag.FromErr(err)
178+
}
179+
}
180+
181+
return ResourceACLRead(ctx, d, m)
182+
}
183+
184+
func ResourceACLDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
185+
api, region, clusterID, err := NewAPIWithRegionAndID(m, d.Id())
186+
if err != nil {
187+
return diag.FromErr(err)
188+
}
189+
190+
_, err = waitCluster(ctx, api, region, clusterID, d.Timeout(schema.TimeoutDelete))
191+
if err != nil && !httperrors.Is404(err) {
192+
return diag.FromErr(err)
193+
}
194+
195+
allIPRange, err := types.ExpandIPNet("0.0.0.0/0")
196+
if err != nil {
197+
return diag.FromErr(err)
198+
}
199+
200+
req := &k8s.SetClusterACLRulesRequest{
201+
Region: region,
202+
ClusterID: clusterID,
203+
ACLs: []*k8s.ACLRuleRequest{
204+
{
205+
IP: &allIPRange,
206+
},
207+
},
208+
}
209+
210+
_, err = api.SetClusterACLRules(req, scw.WithContext(ctx))
211+
if err != nil {
212+
return diag.FromErr(err)
213+
}
214+
215+
_, err = waitCluster(ctx, api, region, clusterID, d.Timeout(schema.TimeoutDelete))
216+
if err != nil && !httperrors.Is404(err) {
217+
return diag.FromErr(err)
218+
}
219+
220+
return nil
221+
}
222+
223+
func expandACL(data []interface{}) ([]*k8s.ACLRuleRequest, error) {
224+
expandedACLs := []*k8s.ACLRuleRequest(nil)
225+
226+
for _, rule := range data {
227+
r := rule.(map[string]interface{})
228+
expandedRule := &k8s.ACLRuleRequest{}
229+
230+
if ipRaw, ipSet := r["ip"]; ipSet && ipRaw != "" {
231+
ip, err := types.ExpandIPNet(ipRaw.(string))
232+
if err != nil {
233+
return nil, err
234+
}
235+
236+
expandedRule.IP = &ip
237+
}
238+
239+
if scwRangesRaw, scwRangesSet := r["scaleway_ranges"]; scwRangesSet && scwRangesRaw.(bool) {
240+
expandedRule.ScalewayRanges = scw.BoolPtr(true)
241+
}
242+
243+
if descriptionRaw, descriptionSet := r["description"]; descriptionSet && descriptionRaw.(string) != "" {
244+
expandedRule.Description = descriptionRaw.(string)
245+
}
246+
247+
expandedACLs = append(expandedACLs, expandedRule)
248+
}
249+
250+
return expandedACLs, nil
251+
}
252+
253+
func flattenACL(rules []*k8s.ACLRule) interface{} {
254+
if rules == nil {
255+
return nil
256+
}
257+
258+
flattenedACLs := []map[string]interface{}(nil)
259+
260+
for _, rule := range rules {
261+
flattenedRule := map[string]interface{}{
262+
"id": rule.ID,
263+
"scaleway_ranges": rule.ScalewayRanges,
264+
"description": rule.Description,
265+
}
266+
if rule.IP != nil {
267+
flattenedRule["ip"] = rule.IP.String()
268+
}
269+
270+
flattenedACLs = append(flattenedACLs, flattenedRule)
271+
}
272+
273+
return flattenedACLs
274+
}

0 commit comments

Comments
 (0)