Skip to content

Commit 47c217c

Browse files
mathieu prigentSkYNewZ
mathieu prigent
authored andcommittedFeb 17, 2023
feat(kube): Configure kubeproxy
1 parent 3f75aeb commit 47c217c

10 files changed

+2119
-156
lines changed
 

‎ovh/data_cloud_project_kube.go

+150-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/url"
77

88
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
9+
"github.com/ovh/terraform-provider-ovh/ovh/helpers"
910
)
1011

1112
func dataSourceCloudProjectKube() *schema.Resource {
@@ -29,28 +30,78 @@ func dataSourceCloudProjectKube() *schema.Resource {
2930
Type: schema.TypeString,
3031
Optional: true,
3132
},
32-
kubeClusterCustomizationKey: {
33+
kubeClusterProxyModeKey: {
34+
Type: schema.TypeString,
35+
Optional: true,
36+
ForceNew: true,
37+
ValidateFunc: helpers.ValidateEnum([]string{"iptables", "ipvs"}),
38+
},
39+
kubeClusterCustomizationApiServerKey: {
3340
Type: schema.TypeSet,
3441
Computed: true,
3542
Optional: true,
43+
// Required: true,
3644
ForceNew: false,
37-
MaxItems: 1,
45+
// MaxItems: 1,
46+
Set: CustomSchemaSetFunc(),
3847
Elem: &schema.Resource{
3948
Schema: map[string]*schema.Schema{
40-
"apiserver": {
49+
"admissionplugins": {
4150
Type: schema.TypeSet,
4251
Computed: true,
4352
Optional: true,
53+
// Required: true,
4454
ForceNew: false,
45-
MaxItems: 1,
55+
// MaxItems: 1,
56+
Set: CustomSchemaSetFunc(),
57+
Elem: &schema.Resource{
58+
Schema: map[string]*schema.Schema{
59+
"enabled": {
60+
Type: schema.TypeList,
61+
Computed: true,
62+
Optional: true,
63+
// Required: true,
64+
ForceNew: false,
65+
Elem: &schema.Schema{Type: schema.TypeString},
66+
},
67+
"disabled": {
68+
Type: schema.TypeList,
69+
Computed: true,
70+
Optional: true,
71+
// Required: true,
72+
ForceNew: false,
73+
Elem: &schema.Schema{Type: schema.TypeString},
74+
},
75+
},
76+
},
77+
},
78+
},
79+
},
80+
},
81+
kubeClusterCustomization: {
82+
Type: schema.TypeSet,
83+
Computed: true,
84+
Optional: true,
85+
ForceNew: false,
86+
Set: CustomSchemaSetFunc(),
87+
Deprecated: fmt.Sprintf("Use %s instead", kubeClusterCustomizationApiServerKey),
88+
Elem: &schema.Resource{
89+
Schema: map[string]*schema.Schema{
90+
"apiserver": {
91+
Type: schema.TypeSet,
92+
Computed: true,
93+
Optional: true,
94+
ForceNew: false,
95+
Set: CustomSchemaSetFunc(),
96+
Deprecated: fmt.Sprintf("Use %s instead", kubeClusterCustomizationApiServerKey),
4697
Elem: &schema.Resource{
4798
Schema: map[string]*schema.Schema{
4899
"admissionplugins": {
49100
Type: schema.TypeSet,
50101
Computed: true,
51102
Optional: true,
52103
ForceNew: false,
53-
MaxItems: 1,
104+
Set: CustomSchemaSetFunc(),
54105
Elem: &schema.Resource{
55106
Schema: map[string]*schema.Schema{
56107
"enabled": {
@@ -76,6 +127,98 @@ func dataSourceCloudProjectKube() *schema.Resource {
76127
},
77128
},
78129
},
130+
kubeClusterCustomizationKubeProxyKey: {
131+
Type: schema.TypeSet,
132+
Computed: false,
133+
Optional: true,
134+
ForceNew: false,
135+
MaxItems: 1,
136+
Elem: &schema.Resource{
137+
Schema: map[string]*schema.Schema{
138+
"iptables": {
139+
Type: schema.TypeSet,
140+
Computed: false,
141+
Optional: true,
142+
ForceNew: false,
143+
MaxItems: 1,
144+
Set: CustomIPVSIPTablesSchemaSetFunc(),
145+
Elem: &schema.Resource{
146+
Schema: map[string]*schema.Schema{
147+
"min_sync_period": {
148+
Type: schema.TypeString,
149+
Computed: false,
150+
Optional: true,
151+
ForceNew: false,
152+
ValidateFunc: helpers.ValidateRFC3339Duration,
153+
},
154+
"sync_period": {
155+
Type: schema.TypeString,
156+
Computed: false,
157+
Optional: true,
158+
ForceNew: false,
159+
ValidateFunc: helpers.ValidateRFC3339Duration,
160+
},
161+
},
162+
},
163+
},
164+
"ipvs": {
165+
Type: schema.TypeSet,
166+
Computed: false,
167+
Optional: true,
168+
ForceNew: false,
169+
MaxItems: 1,
170+
Set: CustomIPVSIPTablesSchemaSetFunc(),
171+
Elem: &schema.Resource{
172+
Schema: map[string]*schema.Schema{
173+
"min_sync_period": {
174+
Type: schema.TypeString,
175+
Computed: false,
176+
Optional: true,
177+
ForceNew: false,
178+
ValidateFunc: helpers.ValidateRFC3339Duration,
179+
},
180+
"sync_period": {
181+
Type: schema.TypeString,
182+
Computed: false,
183+
Optional: true,
184+
ForceNew: false,
185+
ValidateFunc: helpers.ValidateRFC3339Duration,
186+
},
187+
"scheduler": {
188+
Type: schema.TypeString,
189+
Computed: false,
190+
Optional: true,
191+
ForceNew: false,
192+
ValidateFunc: helpers.ValidateEnum([]string{"rr", "lc", "dh", "sh", "sed", "nq"}),
193+
},
194+
"tcp_fin_timeout": {
195+
Type: schema.TypeString,
196+
Computed: false,
197+
Optional: true,
198+
ForceNew: false,
199+
ValidateFunc: helpers.ValidateRFC3339Duration,
200+
},
201+
"tcp_timeout": {
202+
Type: schema.TypeString,
203+
Computed: false,
204+
Optional: true,
205+
ForceNew: false,
206+
ValidateFunc: helpers.ValidateRFC3339Duration,
207+
},
208+
"udp_timeout": {
209+
Type: schema.TypeString,
210+
Computed: false,
211+
Optional: true,
212+
ForceNew: false,
213+
ValidateFunc: helpers.ValidateRFC3339Duration,
214+
},
215+
},
216+
},
217+
},
218+
},
219+
},
220+
},
221+
79222
"private_network_id": {
80223
Type: schema.TypeString,
81224
Computed: true,
@@ -132,12 +275,11 @@ func dataSourceCloudProjectKubeRead(d *schema.ResourceData, meta interface{}) er
132275
url.PathEscape(serviceName),
133276
url.PathEscape(kubeId),
134277
)
135-
err := config.OVHClient.Get(endpoint, res)
136-
if err != nil {
278+
if err := config.OVHClient.Get(endpoint, res); err != nil {
137279
return fmt.Errorf("Error calling %s:\n\t %q", endpoint, err)
138280
}
139281

140-
for k, v := range res.ToMap() {
282+
for k, v := range res.ToMap(d) {
141283
if k != "id" {
142284
d.Set(k, v)
143285
} else {

‎ovh/data_cloud_project_kube_test.go

+75-6
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,58 @@ func TestAccCloudProjectKubeDataSource_basic(t *testing.T) {
2525

2626
resource.Test(t, resource.TestCase{
2727
PreCheck: func() {
28+
testAccPreCheckCloud(t)
29+
testAccCheckCloudProjectExists(t)
2830
testAccPreCheckKubernetes(t)
2931
},
3032
Providers: testAccProviders,
3133
Steps: []resource.TestStep{
3234
{
3335
Config: config,
3436
Check: resource.ComposeTestCheckFunc(
35-
resource.TestCheckResourceAttr(
36-
"data.ovh_cloud_project_kube.cluster", "region", region),
37-
resource.TestCheckResourceAttr(
38-
"data.ovh_cloud_project_kube.cluster", "name", name),
39-
resource.TestMatchResourceAttr(
40-
"data.ovh_cloud_project_kube.cluster", "version", matchVersion),
37+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "region", region),
38+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "name", name),
39+
resource.TestMatchResourceAttr("data.ovh_cloud_project_kube.cluster", "version", matchVersion),
40+
),
41+
},
42+
},
43+
})
44+
}
45+
46+
func TestAccCloudProjectKubeDataSource_kubeProxy(t *testing.T) {
47+
name := acctest.RandomWithPrefix(test_prefix)
48+
region := os.Getenv("OVH_CLOUD_PROJECT_KUBE_REGION_TEST")
49+
config := fmt.Sprintf(
50+
testAccCloudProjectKubeDatasourceKubeProxyConfig,
51+
os.Getenv("OVH_CLOUD_PROJECT_SERVICE_TEST"),
52+
name,
53+
region,
54+
)
55+
56+
resource.Test(t, resource.TestCase{
57+
PreCheck: func() {
58+
testAccPreCheckCloud(t)
59+
testAccCheckCloudProjectExists(t)
60+
testAccPreCheckKubernetes(t)
61+
},
62+
Providers: testAccProviders,
63+
Steps: []resource.TestStep{
64+
{
65+
Config: config,
66+
Check: resource.ComposeTestCheckFunc(
67+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "region", region),
68+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "name", name),
69+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "kube_proxy_mode", "ipvs"),
70+
71+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "customization_kube_proxy.0.iptables.0.min_sync_period", "PT30S"),
72+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "customization_kube_proxy.0.iptables.0.sync_period", "PT30S"),
73+
74+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "customization_kube_proxy.0.ipvs.0.min_sync_period", "PT30S"),
75+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "customization_kube_proxy.0.ipvs.0.sync_period", "PT30S"),
76+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "customization_kube_proxy.0.ipvs.0.scheduler", "rr"),
77+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "customization_kube_proxy.0.ipvs.0.tcp_fin_timeout", "PT30S"),
78+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "customization_kube_proxy.0.ipvs.0.tcp_timeout", "PT30S"),
79+
resource.TestCheckResourceAttr("data.ovh_cloud_project_kube.cluster", "customization_kube_proxy.0.ipvs.0.udp_timeout", "PT30S"),
4180
),
4281
},
4382
},
@@ -57,3 +96,33 @@ data "ovh_cloud_project_kube" "cluster" {
5796
kube_id = ovh_cloud_project_kube.cluster.id
5897
}
5998
`
99+
100+
var testAccCloudProjectKubeDatasourceKubeProxyConfig = `
101+
resource "ovh_cloud_project_kube" "cluster" {
102+
service_name = "%s"
103+
name = "%s"
104+
region = "%s"
105+
106+
kube_proxy_mode = "ipvs"
107+
customization_kube_proxy {
108+
iptables {
109+
min_sync_period = "PT30S"
110+
sync_period = "PT30S"
111+
}
112+
113+
ipvs {
114+
min_sync_period = "PT30S"
115+
sync_period = "PT30S"
116+
scheduler = "rr"
117+
tcp_fin_timeout = "PT30S"
118+
tcp_timeout = "PT30S"
119+
udp_timeout = "PT30S"
120+
}
121+
}
122+
}
123+
124+
data "ovh_cloud_project_kube" "cluster" {
125+
service_name = ovh_cloud_project_kube.cluster.service_name
126+
kube_id = ovh_cloud_project_kube.cluster.id
127+
}
128+
`

‎ovh/helpers/helpers.go

+9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1111
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1212
"github.com/ovh/go-ovh/ovh"
13+
"github.com/ybriffa/rfc3339"
1314
)
1415

1516
func ValidateIpBlock(value string) error {
@@ -186,6 +187,14 @@ func ValidateDedicatedCephStatus(value string) error {
186187
})
187188
}
188189

190+
// ValidateRFC3339Duration implements schema.SchemaValidateFunc for RFC3339 durations.
191+
func ValidateRFC3339Duration(i interface{}, _ string) (_ []string, errors []error) {
192+
if _, err := rfc3339.ParseDuration(i.(string)); err != nil {
193+
errors = append(errors, err)
194+
}
195+
return
196+
}
197+
189198
func ValidateDedicatedCephACLFamily(value string) error {
190199
return ValidateStringEnum(value, []string{
191200
"IPv4",

‎ovh/resource_cloud_project_kube.go

+246-31
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ovh
33
import (
44
"fmt"
55
"log"
6+
"sort"
67
"strings"
78
"time"
89

@@ -19,7 +20,12 @@ const (
1920
kubeClusterPrivateNetworkConfigurationKey = "private_network_configuration"
2021
kubeClusterUpdatePolicyKey = "update_policy"
2122
kubeClusterVersionKey = "version"
22-
kubeClusterCustomizationKey = "customization"
23+
24+
kubeClusterProxyModeKey = "kube_proxy_mode"
25+
26+
kubeClusterCustomization = "customization" // Deprecated
27+
kubeClusterCustomizationApiServerKey = "customization_apiserver"
28+
kubeClusterCustomizationKubeProxyKey = "customization_kube_proxy"
2329
)
2430

2531
func resourceCloudProjectKube() *schema.Resource {
@@ -59,28 +65,68 @@ func resourceCloudProjectKube() *schema.Resource {
5965
Optional: true,
6066
ForceNew: false,
6167
},
62-
kubeClusterCustomizationKey: {
63-
Type: schema.TypeSet,
64-
Computed: true,
65-
Optional: true,
66-
ForceNew: false,
67-
MaxItems: 1,
68+
kubeClusterCustomizationApiServerKey: {
69+
Type: schema.TypeSet,
70+
Computed: true,
71+
Optional: true,
72+
ForceNew: false,
73+
Set: CustomSchemaSetFunc(),
74+
ConflictsWith: []string{kubeClusterCustomization},
6875
Elem: &schema.Resource{
6976
Schema: map[string]*schema.Schema{
70-
"apiserver": {
77+
"admissionplugins": {
7178
Type: schema.TypeSet,
7279
Computed: true,
7380
Optional: true,
7481
ForceNew: false,
75-
MaxItems: 1,
82+
Set: CustomApiServerAdmissionPluginsSchemaSetFunc(),
83+
Elem: &schema.Resource{
84+
Schema: map[string]*schema.Schema{
85+
"enabled": {
86+
Type: schema.TypeList,
87+
Computed: true,
88+
Optional: true,
89+
ForceNew: false,
90+
Elem: &schema.Schema{Type: schema.TypeString},
91+
},
92+
"disabled": {
93+
Type: schema.TypeList,
94+
Computed: true,
95+
Optional: true,
96+
ForceNew: false,
97+
Elem: &schema.Schema{Type: schema.TypeString},
98+
},
99+
},
100+
},
101+
},
102+
},
103+
},
104+
},
105+
kubeClusterCustomization: {
106+
Type: schema.TypeSet,
107+
Computed: true,
108+
Optional: true,
109+
ForceNew: false,
110+
Set: CustomSchemaSetFunc(),
111+
ConflictsWith: []string{kubeClusterCustomizationApiServerKey},
112+
Deprecated: fmt.Sprintf("Use %s instead", kubeClusterCustomizationApiServerKey),
113+
Elem: &schema.Resource{
114+
Schema: map[string]*schema.Schema{
115+
"apiserver": {
116+
Type: schema.TypeSet,
117+
Computed: true,
118+
Optional: true,
119+
ForceNew: false,
120+
Set: CustomSchemaSetFunc(),
121+
Deprecated: fmt.Sprintf("Use %s instead", kubeClusterCustomizationApiServerKey),
76122
Elem: &schema.Resource{
77123
Schema: map[string]*schema.Schema{
78124
"admissionplugins": {
79125
Type: schema.TypeSet,
80126
Computed: true,
81127
Optional: true,
82128
ForceNew: false,
83-
MaxItems: 1,
129+
Set: CustomApiServerAdmissionPluginsSchemaSetFunc(),
84130
Elem: &schema.Resource{
85131
Schema: map[string]*schema.Schema{
86132
"enabled": {
@@ -106,11 +152,110 @@ func resourceCloudProjectKube() *schema.Resource {
106152
},
107153
},
108154
},
155+
kubeClusterCustomizationKubeProxyKey: {
156+
Type: schema.TypeSet,
157+
Computed: false,
158+
Optional: true,
159+
ForceNew: false,
160+
MaxItems: 1,
161+
Elem: &schema.Resource{
162+
Schema: map[string]*schema.Schema{
163+
"iptables": {
164+
Type: schema.TypeSet,
165+
Computed: false,
166+
Optional: true,
167+
ForceNew: false,
168+
MaxItems: 1,
169+
Set: CustomIPVSIPTablesSchemaSetFunc(),
170+
Elem: &schema.Resource{
171+
Schema: map[string]*schema.Schema{
172+
"min_sync_period": {
173+
Type: schema.TypeString,
174+
Computed: false,
175+
Optional: true,
176+
ForceNew: false,
177+
ValidateFunc: helpers.ValidateRFC3339Duration,
178+
},
179+
"sync_period": {
180+
Type: schema.TypeString,
181+
Computed: false,
182+
Optional: true,
183+
ForceNew: false,
184+
ValidateFunc: helpers.ValidateRFC3339Duration,
185+
},
186+
},
187+
},
188+
},
189+
"ipvs": {
190+
Type: schema.TypeSet,
191+
Computed: false,
192+
Optional: true,
193+
ForceNew: false,
194+
MaxItems: 1,
195+
Set: CustomIPVSIPTablesSchemaSetFunc(),
196+
Elem: &schema.Resource{
197+
Schema: map[string]*schema.Schema{
198+
"min_sync_period": {
199+
Type: schema.TypeString,
200+
Computed: false,
201+
Optional: true,
202+
ForceNew: false,
203+
ValidateFunc: helpers.ValidateRFC3339Duration,
204+
},
205+
"sync_period": {
206+
Type: schema.TypeString,
207+
Computed: false,
208+
Optional: true,
209+
ForceNew: false,
210+
ValidateFunc: helpers.ValidateRFC3339Duration,
211+
},
212+
"scheduler": {
213+
Type: schema.TypeString,
214+
Computed: false,
215+
Optional: true,
216+
ForceNew: false,
217+
ValidateFunc: helpers.ValidateEnum([]string{"rr", "lc", "dh", "sh", "sed", "nq"}),
218+
},
219+
"tcp_fin_timeout": {
220+
Type: schema.TypeString,
221+
Computed: false,
222+
Optional: true,
223+
ForceNew: false,
224+
ValidateFunc: helpers.ValidateRFC3339Duration,
225+
},
226+
"tcp_timeout": {
227+
Type: schema.TypeString,
228+
Computed: false,
229+
Optional: true,
230+
ForceNew: false,
231+
ValidateFunc: helpers.ValidateRFC3339Duration,
232+
},
233+
"udp_timeout": {
234+
Type: schema.TypeString,
235+
Computed: false,
236+
Optional: true,
237+
ForceNew: false,
238+
ValidateFunc: helpers.ValidateRFC3339Duration,
239+
},
240+
},
241+
},
242+
},
243+
},
244+
},
245+
},
246+
109247
kubeClusterPrivateNetworkIDKey: {
110248
Type: schema.TypeString,
111249
Optional: true,
112250
ForceNew: true,
113251
},
252+
kubeClusterProxyModeKey: {
253+
Type: schema.TypeString,
254+
Optional: true,
255+
ForceNew: true,
256+
Computed: true,
257+
ValidateFunc: helpers.ValidateEnum([]string{"iptables", "ipvs"}),
258+
},
114259
kubeClusterPrivateNetworkConfigurationKey: {
115260
Type: schema.TypeSet,
116261
Optional: true,
@@ -207,6 +352,60 @@ func resourceCloudProjectKube() *schema.Resource {
207352
}
208353
}
209354

355+
// CustomIPVSIPTablesSchemaSetFunc is a custom schema.SchemaSetFunc for IPVS and IPTables
356+
// block configuration.
357+
//
358+
// Even if setting in the API `PT0S`, it returns `P0D` which is exactly the same duration but
359+
// induce issue when calculating hashset.
360+
//
361+
// Moreover, we cannot use DiffSuppressFunc because even if the diff is removed the hashset is still different.
362+
//
363+
// Using schema.StateFunc does not help because of internal terraform execution diff calculation
364+
// order.
365+
func CustomIPVSIPTablesSchemaSetFunc() schema.SchemaSetFunc {
366+
return func(i interface{}) int {
367+
for k, v := range i.(map[string]interface{}) {
368+
if v == "P0D" {
369+
i.(map[string]interface{})[k] = "PT0S"
370+
}
371+
}
372+
373+
out := fmt.Sprintf("%#v", i)
374+
return schema.HashString(out)
375+
}
376+
}
377+
378+
func CustomSchemaSetFunc() schema.SchemaSetFunc {
379+
return func(i interface{}) int {
380+
out := fmt.Sprintf("%#v", i)
381+
return schema.HashString(out)
382+
}
383+
}
384+
385+
// CustomApiServerAdmissionPluginsSchemaSetFunc is a custom schema.SchemaSetFunc for api_server.admission_plugins
386+
// It orders plugins by alphabetical order to avoid hashset diff
387+
func CustomApiServerAdmissionPluginsSchemaSetFunc() schema.SchemaSetFunc {
388+
return func(i interface{}) int {
389+
enabled := i.(map[string]interface{})["enabled"].([]interface{})
390+
disabled := i.(map[string]interface{})["disabled"].([]interface{})
391+
392+
orderSliceByAlphabeticalOrder := func(s []interface{}) {
393+
sort.Slice(s, func(i, j int) bool {
394+
return s[i].(string) < s[j].(string)
395+
})
396+
}
397+
398+
orderSliceByAlphabeticalOrder(enabled)
399+
orderSliceByAlphabeticalOrder(disabled)
400+
401+
i.(map[string]interface{})["enabled"] = enabled
402+
i.(map[string]interface{})["disabled"] = disabled
403+
404+
out := fmt.Sprintf("%#v", i)
405+
return schema.HashString(out)
406+
}
407+
}
408+
210409
func resourceCloudProjectKubeImportState(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
211410
givenId := d.Id()
212411
splitId := strings.SplitN(givenId, "/", 2)
@@ -232,31 +431,29 @@ func resourceCloudProjectKubeCreate(d *schema.ResourceData, meta interface{}) er
232431
config := meta.(*Config)
233432
serviceName := d.Get("service_name").(string)
234433

235-
endpoint := fmt.Sprintf("/cloud/project/%s/kube", serviceName)
236-
params := (&CloudProjectKubeCreateOpts{}).FromResource(d)
434+
params := new(CloudProjectKubeCreateOpts)
435+
params.FromResource(d)
436+
237437
res := &CloudProjectKubeResponse{}
238438

239-
log.Printf("[DEBUG] Will create kube: %+v", params)
240-
err := config.OVHClient.Post(endpoint, params, res)
241-
if err != nil {
439+
log.Printf("[DEBUG] Will create kube: %s", params)
440+
endpoint := fmt.Sprintf("/cloud/project/%s/kube", serviceName)
441+
if err := config.OVHClient.Post(endpoint, params, res); err != nil {
242442
return fmt.Errorf("calling Post %s with params %s:\n\t %w", endpoint, params, err)
243443
}
244444

245-
// This is a fix for a weird bug where the kube is not immediately available on API
246445
log.Printf("[DEBUG] Waiting for kube %s to be available", res.Id)
247446
endpoint = fmt.Sprintf("/cloud/project/%s/kube/%s", serviceName, res.Id)
248-
err = helpers.WaitAvailable(config.OVHClient, endpoint, 2*time.Minute)
249-
if err != nil {
447+
if err := helpers.WaitAvailable(config.OVHClient, endpoint, d.Timeout(schema.TimeoutCreate)); err != nil {
250448
return err
251449
}
252450

253451
log.Printf("[DEBUG] Waiting for kube %s to be READY", res.Id)
254-
err = waitForCloudProjectKubeReady(config.OVHClient, serviceName, res.Id, []string{"INSTALLING"}, []string{"READY"}, d.Timeout(schema.TimeoutCreate))
255-
if err != nil {
452+
if err := waitForCloudProjectKubeReady(config.OVHClient, serviceName, res.Id, []string{"INSTALLING"}, []string{"READY"}, d.Timeout(schema.TimeoutCreate)); err != nil {
256453
return fmt.Errorf("timeout while waiting kube %s to be READY: %w", res.Id, err)
257454
}
258-
log.Printf("[DEBUG] kube %s is READY", res.Id)
259455

456+
log.Printf("[DEBUG] kube %s is READY", res.Id)
260457
d.SetId(res.Id)
261458

262459
return resourceCloudProjectKubeRead(d, meta)
@@ -273,7 +470,8 @@ func resourceCloudProjectKubeRead(d *schema.ResourceData, meta interface{}) erro
273470
if err := config.OVHClient.Get(endpoint, res); err != nil {
274471
return helpers.CheckDeleted(d, err, endpoint)
275472
}
276-
for k, v := range res.ToMap() {
473+
for k, v := range res.ToMap(d) {
474+
log.Printf("[DEBUG] Will set %s to %v", k, v)
277475
if k != "id" {
278476
d.Set(k, v)
279477
} else {
@@ -320,23 +518,40 @@ func resourceCloudProjectKubeUpdate(d *schema.ResourceData, meta interface{}) er
320518
config := meta.(*Config)
321519
serviceName := d.Get("service_name").(string)
322520

323-
if d.HasChange(kubeClusterCustomizationKey) {
324-
_, newValueI := d.GetChange(kubeClusterCustomizationKey)
325-
customization := loadCustomization(newValueI)
521+
// if customization has changed, update it
522+
if d.HasChange(kubeClusterCustomizationApiServerKey) || d.HasChange(kubeClusterCustomization) || d.HasChange(kubeClusterCustomizationKubeProxyKey) {
523+
customization := new(Customization)
326524

327-
endpoint := fmt.Sprintf("/cloud/project/%s/kube/%s/customization", serviceName, d.Id())
328-
err := config.OVHClient.Put(endpoint, CloudProjectKubeUpdateCustomizationOpts{
525+
if d.HasChange(kubeClusterCustomizationKubeProxyKey) {
526+
customization.KubeProxy = loadKubeProxyCustomization(d.Get(kubeClusterCustomizationKubeProxyKey))
527+
}
528+
529+
if d.HasChange(kubeClusterCustomizationApiServerKey) {
530+
_, apiServerCustomization := d.GetChange(kubeClusterCustomizationApiServerKey)
531+
customization.APIServer = loadApiServerCustomization(apiServerCustomization)
532+
}
533+
534+
// deprecated api server customization
535+
if d.HasChange(kubeClusterCustomization) {
536+
_, oldApiServerCustomization := d.GetChange(kubeClusterCustomization)
537+
customization.APIServer = loadDeprecatedApiServerCustomization(oldApiServerCustomization)
538+
}
539+
540+
params := &CloudProjectKubeUpdateCustomizationOpts{
329541
APIServer: customization.APIServer,
330-
}, nil)
331-
if err != nil {
542+
KubeProxy: customization.KubeProxy,
543+
}
544+
545+
endpoint := fmt.Sprintf("/cloud/project/%s/kube/%s/customization", serviceName, d.Id())
546+
if err := config.OVHClient.Put(endpoint, params, nil); err != nil {
332547
return err
333548
}
334549

335550
log.Printf("[DEBUG] Waiting for kube %s to be READY", d.Id())
336-
err = waitForCloudProjectKubeReady(config.OVHClient, serviceName, d.Id(), []string{"REDEPLOYING", "RESETTING"}, []string{"READY"}, d.Timeout(schema.TimeoutUpdate))
337-
if err != nil {
551+
if err := waitForCloudProjectKubeReady(config.OVHClient, serviceName, d.Id(), []string{"REDEPLOYING", "RESETTING"}, []string{"READY"}, d.Timeout(schema.TimeoutUpdate)); err != nil {
338552
return fmt.Errorf("timeout while waiting kube %s to be READY: %w", d.Id(), err)
339553
}
554+
340555
log.Printf("[DEBUG] kube %s is READY", d.Id())
341556
}
342557

‎ovh/resource_cloud_project_kube_nodepool.go

+4-16
Original file line numberDiff line numberDiff line change
@@ -149,23 +149,15 @@ func resourceCloudProjectKubeNodePool() *schema.Resource {
149149
Optional: true,
150150
Type: schema.TypeSet,
151151
MaxItems: 1,
152-
Set: func(i interface{}) int {
153-
out := fmt.Sprintf("%#v", i)
154-
hash := int(schema.HashString(out))
155-
return hash
156-
},
152+
Set: CustomSchemaSetFunc(),
157153
Elem: &schema.Resource{
158154
Schema: map[string]*schema.Schema{
159155
"metadata": {
160156
Description: "metadata",
161157
Optional: true,
162158
Type: schema.TypeSet,
163159
MaxItems: 1,
164-
Set: func(i interface{}) int {
165-
out := fmt.Sprintf("%#v", i)
166-
hash := int(schema.HashString(out))
167-
return hash
168-
},
160+
Set: CustomSchemaSetFunc(),
169161
Elem: &schema.Resource{
170162
Schema: map[string]*schema.Schema{
171163
"finalizers": {
@@ -196,11 +188,7 @@ func resourceCloudProjectKubeNodePool() *schema.Resource {
196188
Optional: true,
197189
Type: schema.TypeSet,
198190
MaxItems: 1,
199-
Set: func(i interface{}) int {
200-
out := fmt.Sprintf("%#v", i)
201-
hash := int(schema.HashString(out))
202-
return hash
203-
},
191+
Set: CustomSchemaSetFunc(),
204192
Elem: &schema.Resource{
205193
Schema: map[string]*schema.Schema{
206194
"unschedulable": {
@@ -322,7 +310,7 @@ func resourceCloudProjectKubeNodePoolUpdate(d *schema.ResourceData, meta interfa
322310
log.Printf("[DEBUG] Will update nodepool: %#v", *params)
323311
err = config.OVHClient.Put(endpoint, params, nil)
324312
if err != nil {
325-
return fmt.Errorf("calling Put %s with params %#v:\n\t %w", endpoint, *params, err)
313+
return fmt.Errorf("calling Put %s with params %v:\n\t %w", endpoint, *params, err)
326314
}
327315

328316
log.Printf("[DEBUG] Waiting for nodepool %s to be READY", d.Id())

‎ovh/resource_cloud_project_kube_test.go

+797-14
Large diffs are not rendered by default.

‎ovh/types_cloud_project_kube.go

+263-37
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package ovh
22

33
import (
44
"fmt"
5+
"log"
56
"strings"
67

78
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -30,69 +31,195 @@ type CloudProjectKubeCreateOpts struct {
3031
Version *string `json:"version,omitempty"`
3132
UpdatePolicy *string `json:"updatePolicy,omitempty"`
3233
Customization *Customization `json:"customization,omitempty"`
34+
KubeProxyMode *string `json:"kubeProxyMode,omitempty"`
3335
}
3436

3537
type Customization struct {
36-
APIServer *APIServer `json:"apiServer,omitempty"`
38+
APIServer *APIServer `json:"apiServer,omitempty"`
39+
KubeProxy *kubeProxyCustomization `json:"kubeProxy,omitempty"`
3740
}
3841

3942
type APIServer struct {
4043
AdmissionPlugins *AdmissionPlugins `json:"admissionPlugins,omitempty"`
4144
}
4245

46+
type kubeProxyCustomization struct {
47+
IPTables *kubeProxyCustomizationIPTables `json:"iptables,omitempty"`
48+
IPVS *kubeProxyCustomizationIPVS `json:"ipvs,omitempty"`
49+
}
50+
51+
type kubeProxyCustomizationIPTables struct {
52+
MinSyncPeriod *string `json:"minSyncPeriod,omitempty"`
53+
SyncPeriod *string `json:"syncPeriod,omitempty"`
54+
}
55+
56+
type kubeProxyCustomizationIPVS struct {
57+
MinSyncPeriod *string `json:"minSyncPeriod,omitempty"`
58+
Scheduler *string `json:"scheduler,omitempty"`
59+
SyncPeriod *string `json:"syncPeriod,omitempty"`
60+
TCPFinTimeout *string `json:"tcpFinTimeout,omitempty"`
61+
TCPTimeout *string `json:"tcpTimeout,omitempty"`
62+
UDPTimeout *string `json:"udpTimeout,omitempty"`
63+
}
64+
4365
type AdmissionPlugins struct {
4466
Enabled *[]string `json:"enabled,omitempty"`
4567
Disabled *[]string `json:"disabled,omitempty"`
4668
}
4769

48-
func (opts *CloudProjectKubeCreateOpts) FromResource(d *schema.ResourceData) *CloudProjectKubeCreateOpts {
70+
func (opts *CloudProjectKubeCreateOpts) FromResource(d *schema.ResourceData) {
4971
opts.Region = d.Get("region").(string)
5072
opts.Version = helpers.GetNilStringPointerFromData(d, "version")
5173
opts.Name = helpers.GetNilStringPointerFromData(d, "name")
5274
opts.UpdatePolicy = helpers.GetNilStringPointerFromData(d, "update_policy")
5375
opts.PrivateNetworkId = helpers.GetNilStringPointerFromData(d, "private_network_id")
5476
opts.PrivateNetworkConfiguration = loadPrivateNetworkConfiguration(d.Get("private_network_configuration"))
55-
opts.Customization = loadCustomization(d.Get("customization"))
56-
return opts
77+
opts.KubeProxyMode = helpers.GetNilStringPointerFromData(d, kubeClusterProxyModeKey)
78+
79+
opts.Customization = &Customization{
80+
APIServer: nil,
81+
KubeProxy: loadKubeProxyCustomization(d.Get(kubeClusterCustomizationKubeProxyKey)),
82+
}
83+
84+
// load the filled api server customization
85+
// both the new and the deprecated syntax are supported, but they are mutual exclusive
86+
if userIsUsingDeprecatedCustomizationSyntax(d) {
87+
log.Printf("[DEBUG] Using DEPRECATED syntax for api server customization")
88+
opts.Customization.APIServer = loadDeprecatedApiServerCustomization(d.Get(kubeClusterCustomization))
89+
} else {
90+
log.Printf("[DEBUG] Using new syntax for api server customization")
91+
opts.Customization.APIServer = loadApiServerCustomization(d.Get(kubeClusterCustomizationApiServerKey))
92+
}
5793
}
5894

59-
func loadCustomization(i interface{}) *Customization {
60-
if i == nil {
95+
func userIsUsingDeprecatedCustomizationSyntax(d *schema.ResourceData) bool {
96+
funcTypeSetNotNilAndNotEmpty := func(d *schema.ResourceData, key string) bool {
97+
return d.Get(key) != nil && len(d.Get(key).(*schema.Set).List()) > 0
98+
}
99+
100+
return funcTypeSetNotNilAndNotEmpty(d, kubeClusterCustomization)
101+
}
102+
103+
// loadApiServerCustomization reads the api server customization
104+
func loadApiServerCustomization(apiServerAdmissionPlugins interface{}) *APIServer {
105+
if apiServerAdmissionPlugins == nil {
61106
return nil
62107
}
63108

64-
customizationOutput := Customization{
65-
APIServer: &APIServer{
66-
AdmissionPlugins: &AdmissionPlugins{},
67-
},
109+
apiServerOutput := &APIServer{
110+
AdmissionPlugins: &AdmissionPlugins{},
111+
}
112+
113+
// Customization
114+
customizationSet := apiServerAdmissionPlugins.(*schema.Set).List()
115+
if len(customizationSet) > 0 {
116+
customization := customizationSet[0].(map[string]interface{})
117+
admissionPluginsSet := customization["admissionplugins"].(*schema.Set).List()
118+
admissionPlugins := admissionPluginsSet[0].(map[string]interface{})
119+
120+
readApiServerAdmissionPlugins(admissionPlugins, apiServerOutput)
121+
122+
log.Printf("[DEBUG] Enabled admission plugins from new syntax: %v", apiServerOutput.AdmissionPlugins.Enabled)
123+
log.Printf("[DEBUG] Disabled admission plugins from new syntax: %v", apiServerOutput.AdmissionPlugins.Disabled)
124+
}
125+
126+
return apiServerOutput
127+
}
128+
129+
func readApiServerAdmissionPlugins(admissionPlugins map[string]interface{}, apiServerOutput *APIServer) {
130+
// Enabled admission plugins
131+
{
132+
stringArray := admissionPlugins["enabled"].([]interface{})
133+
enabled := make([]string, 0, len(stringArray))
134+
for _, s := range stringArray {
135+
enabled = append(enabled, s.(string))
136+
}
137+
apiServerOutput.AdmissionPlugins.Enabled = &enabled
68138
}
69139

70-
customizationSet := i.(*schema.Set).List()
71-
for _, customization := range customizationSet {
72-
apiServerSet := customization.(map[string]interface{})["apiserver"].(*schema.Set).List()
73-
for _, apiServer := range apiServerSet {
74-
admissionPluginsSet := apiServer.(map[string]interface{})["admissionplugins"].(*schema.Set).List()
75-
for _, admissionPlugins := range admissionPluginsSet {
140+
// Disabled admission plugins
141+
{
142+
stringArray := admissionPlugins["disabled"].([]interface{})
143+
disabled := make([]string, 0, len(stringArray))
144+
for _, s := range stringArray {
145+
disabled = append(disabled, s.(string))
146+
}
147+
apiServerOutput.AdmissionPlugins.Disabled = &disabled
148+
}
149+
}
76150

77-
stringArray := admissionPlugins.(map[string]interface{})["enabled"].([]interface{})
78-
enabled := []string{}
79-
for _, s := range stringArray {
80-
enabled = append(enabled, s.(string))
81-
}
82-
customizationOutput.APIServer.AdmissionPlugins.Enabled = &enabled
151+
// loadDeprecatedApiServerCustomization reads the deprecated api server customization
152+
// Deprecated, should be removed in the future
153+
func loadDeprecatedApiServerCustomization(deprecatedApiServerCustomizationInterface interface{}) *APIServer {
154+
if deprecatedApiServerCustomizationInterface == nil {
155+
return nil
156+
}
157+
158+
apiServerOutput := &APIServer{
159+
AdmissionPlugins: &AdmissionPlugins{},
160+
}
83161

84-
stringArray = admissionPlugins.(map[string]interface{})["disabled"].([]interface{})
85-
disabled := []string{}
86-
for _, s := range stringArray {
87-
disabled = append(disabled, s.(string))
88-
}
89-
customizationOutput.APIServer.AdmissionPlugins.Disabled = &disabled
162+
oldCustomizationSet := deprecatedApiServerCustomizationInterface.(*schema.Set).List()
163+
if len(oldCustomizationSet) > 0 {
164+
oldApiServerCustomization := oldCustomizationSet[0].(map[string]interface{})
165+
oldApiServerCustomizationSet := oldApiServerCustomization["apiserver"].(*schema.Set).List()
90166

167+
if len(oldApiServerCustomizationSet) > 0 {
168+
oldApiServerCustomizationAdmissionPlugins := oldApiServerCustomizationSet[0].(map[string]interface{})
169+
oldApiServerCustomizationAdmissionPluginsSet := oldApiServerCustomizationAdmissionPlugins["admissionplugins"].(*schema.Set).List()
170+
admissionPlugins := oldApiServerCustomizationAdmissionPluginsSet[0].(map[string]interface{})
171+
172+
readApiServerAdmissionPlugins(admissionPlugins, apiServerOutput)
173+
}
174+
}
175+
176+
log.Printf("[DEBUG] Enabled admission plugins from DEPRECATED syntax: %v", apiServerOutput.AdmissionPlugins.Enabled)
177+
log.Printf("[DEBUG] Disabled admission plugins from DEPRECATED syntax: %v", apiServerOutput.AdmissionPlugins.Disabled)
178+
179+
return apiServerOutput
180+
}
181+
182+
// loadKubeProxyCustomization reads the kube proxy customization
183+
func loadKubeProxyCustomization(kubeProxyCustomizationInterface interface{}) *kubeProxyCustomization {
184+
if kubeProxyCustomizationInterface == nil {
185+
return nil
186+
}
187+
188+
kubeProxyOutput := &kubeProxyCustomization{
189+
IPTables: &kubeProxyCustomizationIPTables{},
190+
IPVS: &kubeProxyCustomizationIPVS{},
191+
}
192+
193+
kubeProxySet := kubeProxyCustomizationInterface.(*schema.Set).List()
194+
if len(kubeProxySet) > 0 {
195+
kubeProxy := kubeProxySet[0].(map[string]interface{})
196+
197+
// Nested IPTables customization
198+
{
199+
ipTablesSet := kubeProxy["iptables"].(*schema.Set).List()
200+
if len(ipTablesSet) > 0 {
201+
ipTables := ipTablesSet[0].(map[string]interface{})
202+
kubeProxyOutput.IPTables.MinSyncPeriod = helpers.GetNilStringPointerFromData(ipTables, "min_sync_period")
203+
kubeProxyOutput.IPTables.SyncPeriod = helpers.GetNilStringPointerFromData(ipTables, "sync_period")
204+
}
205+
}
206+
207+
// Nested IPVS customization
208+
{
209+
ipvsSet := kubeProxy["ipvs"].(*schema.Set).List()
210+
if len(ipvsSet) > 0 {
211+
ipvs := ipvsSet[0].(map[string]interface{})
212+
kubeProxyOutput.IPVS.MinSyncPeriod = helpers.GetNilStringPointerFromData(ipvs, "min_sync_period")
213+
kubeProxyOutput.IPVS.Scheduler = helpers.GetNilStringPointerFromData(ipvs, "scheduler")
214+
kubeProxyOutput.IPVS.SyncPeriod = helpers.GetNilStringPointerFromData(ipvs, "sync_period")
215+
kubeProxyOutput.IPVS.TCPFinTimeout = helpers.GetNilStringPointerFromData(ipvs, "tcp_fin_timeout")
216+
kubeProxyOutput.IPVS.TCPTimeout = helpers.GetNilStringPointerFromData(ipvs, "tcp_timeout")
217+
kubeProxyOutput.IPVS.UDPTimeout = helpers.GetNilStringPointerFromData(ipvs, "udp_timeout")
91218
}
92219
}
93220
}
94221

95-
return &customizationOutput
222+
return kubeProxyOutput
96223
}
97224

98225
func loadPrivateNetworkConfiguration(i interface{}) *privateNetworkConfiguration {
@@ -110,8 +237,19 @@ func loadPrivateNetworkConfiguration(i interface{}) *privateNetworkConfiguration
110237
return &pncOutput
111238
}
112239

113-
func (s *CloudProjectKubeCreateOpts) String() string {
114-
return fmt.Sprintf("%s(%s): %s", *s.Name, s.Region, *s.Version)
240+
func (opts *CloudProjectKubeCreateOpts) String() string {
241+
var str string
242+
if opts.Name != nil {
243+
str = *opts.Name
244+
}
245+
246+
str += fmt.Sprintf(" (%s)", opts.Region)
247+
248+
if opts.Version != nil {
249+
str += fmt.Sprintf(": %s", *opts.Version)
250+
}
251+
252+
return str
115253
}
116254

117255
type CloudProjectKubeResponse struct {
@@ -128,9 +266,10 @@ type CloudProjectKubeResponse struct {
128266
Url string `json:"url"`
129267
Version string `json:"version"`
130268
Customization Customization `json:"customization"`
269+
KubeProxyMode string `json:"kubeProxyMode"`
131270
}
132271

133-
func (v CloudProjectKubeResponse) ToMap() map[string]interface{} {
272+
func (v *CloudProjectKubeResponse) ToMap(d *schema.ResourceData) map[string]interface{} {
134273
obj := make(map[string]interface{})
135274
obj["control_plane_is_up_to_date"] = v.ControlPlaneIsUpToDate
136275
obj["id"] = v.Id
@@ -144,7 +283,81 @@ func (v CloudProjectKubeResponse) ToMap() map[string]interface{} {
144283
obj["update_policy"] = v.UpdatePolicy
145284
obj["url"] = v.Url
146285
obj["version"] = v.Version[:strings.LastIndex(v.Version, ".")]
147-
obj["customization"] = []map[string]interface{}{
286+
obj[kubeClusterProxyModeKey] = v.KubeProxyMode
287+
288+
if v.Customization.APIServer != nil {
289+
if userIsUsingDeprecatedCustomizationSyntax(d) {
290+
loadDeprecatedApiServerCustomizationToMap(obj, v)
291+
} else {
292+
loadApiServerCustomizationToMap(obj, v)
293+
}
294+
}
295+
296+
if v.Customization.KubeProxy != nil {
297+
loadKubeProxyCustomizationToMap(obj, v)
298+
}
299+
300+
return obj
301+
}
302+
303+
func loadKubeProxyCustomizationToMap(obj map[string]interface{}, v *CloudProjectKubeResponse) {
304+
obj[kubeClusterCustomizationKubeProxyKey] = []map[string]interface{}{{}}
305+
306+
if v.Customization.KubeProxy.IPTables != nil {
307+
data := make(map[string]interface{})
308+
if vv := v.Customization.KubeProxy.IPTables.MinSyncPeriod; vv != nil && *vv != "" {
309+
data["min_sync_period"] = vv
310+
}
311+
312+
if vv := v.Customization.KubeProxy.IPTables.SyncPeriod; vv != nil && *vv != "" {
313+
data["sync_period"] = vv
314+
}
315+
316+
if len(data) > 0 {
317+
obj[kubeClusterCustomizationKubeProxyKey].([]map[string]interface{})[0]["iptables"] = []map[string]interface{}{data}
318+
}
319+
}
320+
321+
if v.Customization.KubeProxy.IPVS != nil {
322+
data := make(map[string]interface{})
323+
if vv := v.Customization.KubeProxy.IPVS.MinSyncPeriod; vv != nil && *vv != "" {
324+
data["min_sync_period"] = vv
325+
}
326+
327+
if vv := v.Customization.KubeProxy.IPVS.Scheduler; vv != nil && *vv != "" {
328+
data["scheduler"] = vv
329+
}
330+
331+
if vv := v.Customization.KubeProxy.IPVS.SyncPeriod; vv != nil && *vv != "" {
332+
data["sync_period"] = vv
333+
}
334+
335+
if vv := v.Customization.KubeProxy.IPVS.TCPFinTimeout; vv != nil && *vv != "" {
336+
data["tcp_fin_timeout"] = vv
337+
}
338+
339+
if vv := v.Customization.KubeProxy.IPVS.TCPTimeout; vv != nil && *vv != "" {
340+
data["tcp_timeout"] = vv
341+
}
342+
343+
if vv := v.Customization.KubeProxy.IPVS.UDPTimeout; vv != nil && *vv != "" {
344+
data["udp_timeout"] = vv
345+
}
346+
347+
if len(data) > 0 {
348+
obj[kubeClusterCustomizationKubeProxyKey].([]map[string]interface{})[0]["ipvs"] = []map[string]interface{}{data}
349+
}
350+
}
351+
352+
// Delete entire customization_kube_proxy if empty
353+
if len(obj[kubeClusterCustomizationKubeProxyKey].([]map[string]interface{})[0]) == 0 {
354+
delete(obj, kubeClusterCustomizationKubeProxyKey)
355+
}
356+
}
357+
358+
// Deprecated: use loadApiServerCustomizationToMap instead
359+
func loadDeprecatedApiServerCustomizationToMap(obj map[string]interface{}, v *CloudProjectKubeResponse) {
360+
obj[kubeClusterCustomization] = []map[string]interface{}{
148361
{
149362
"apiserver": []map[string]interface{}{
150363
{
@@ -158,11 +371,23 @@ func (v CloudProjectKubeResponse) ToMap() map[string]interface{} {
158371
},
159372
},
160373
}
161-
return obj
162374
}
163375

164-
func (s *CloudProjectKubeResponse) String() string {
165-
return fmt.Sprintf("%s(%s): %s", s.Name, s.Id, s.Status)
376+
func loadApiServerCustomizationToMap(obj map[string]interface{}, v *CloudProjectKubeResponse) {
377+
obj[kubeClusterCustomizationApiServerKey] = []map[string]interface{}{
378+
{
379+
"admissionplugins": []map[string]interface{}{
380+
{
381+
"enabled": v.Customization.APIServer.AdmissionPlugins.Enabled,
382+
"disabled": v.Customization.APIServer.AdmissionPlugins.Disabled,
383+
},
384+
},
385+
},
386+
}
387+
}
388+
389+
func (v *CloudProjectKubeResponse) String() string {
390+
return fmt.Sprintf("%s(%s): %s", v.Name, v.Id, v.Status)
166391
}
167392

168393
type CloudProjectKubeKubeConfigResponse struct {
@@ -183,7 +408,8 @@ type CloudProjectKubeUpdatePNCOpts struct {
183408
}
184409

185410
type CloudProjectKubeUpdateCustomizationOpts struct {
186-
APIServer *APIServer `json:"apiServer"`
411+
APIServer *APIServer `json:"apiServer,omitempty"`
412+
KubeProxy *kubeProxyCustomization `json:"kubeProxy,omitempty"`
187413
}
188414

189415
type CloudProjectKubeNodeResponse struct {

‎ovh/types_cloud_project_kube_test.go

+475
Large diffs are not rendered by default.

‎website/docs/d/cloud_project_kube.html.markdown

+20-6
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,29 @@ The following attributes are exported:
4040
* `region` - The OVHcloud public cloud region ID of the managed kubernetes cluster.
4141
* `version` - Kubernetes version of the managed kubernetes cluster.
4242
* `private_network_id` - OpenStack private network (or vrack) ID to use.
43-
* `control_plane_is_up_to_date` - True if control-plane is up to date.
44-
* `is_up_to_date` - True if all nodes and control-plane are up to date.
43+
* `control_plane_is_up_to_date` - True if control-plane is up-to-date.
44+
* `is_up_to_date` - True if all nodes and control-plane are up-to-date.
4545
* `next_upgrade_versions` - Kubernetes versions available for upgrade.
4646
* `nodes_url` - Cluster nodes URL.
4747
* `status` - Cluster status. Should be normally set to 'READY'.
4848
* `update_policy` - Cluster update policy. Choose between [ALWAYS_UPDATE,MINIMAL_DOWNTIME,NEVER_UPDATE]'.
4949
* `url` - Management URL of your cluster.
50-
* `customization` - Customer customization object
50+
* `kube_proxy_mode` - Selected mode for kube-proxy.
51+
* `customization` - **Deprecated** (Optional) Use `customization_apiserver` and `customization_kube_proxy` instead. Kubernetes cluster customization
5152
* `apiserver` - Kubernetes API server customization
52-
* `admissionplugins` - Kubernetes API server admission plugins customization
53-
* `enabled` - Array of admission plugins enabled, default is ["NodeRestriction","AlwaysPulImages"] and only these admission plugins can be enabled at this time.
54-
* `disabled` - Array of admission plugins disabled, default is [] and only AlwaysPulImages can be disabled at this time.
53+
* `kube_proxy` - Kubernetes kube-proxy customization
54+
* `customization_apiserver` - Kubernetes API server customization
55+
* `admissionplugins` - Kubernetes API server admission plugins customization
56+
* `enabled` - Array of admission plugins enabled, default is ["NodeRestriction","AlwaysPulImages"] and only these admission plugins can be enabled at this time.
57+
* `disabled` - Array of admission plugins disabled, default is [] and only AlwaysPulImages can be disabled at this time.
58+
* `customization_kube_proxy` - Kubernetes kube-proxy customization
59+
* `iptables` - Kubernetes cluster kube-proxy customization of iptables specific config.
60+
* `sync_period` - Minimum period that iptables rules are refreshed, in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration format.
61+
* `min_sync_period` - Period that iptables rules are refreshed, in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration format.
62+
* `ipvs` - Kubernetes cluster kube-proxy customization of IPVS specific config (durations format is [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration.
63+
* `sync_period` - Minimum period that IPVS rules are refreshed, in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration format.
64+
* `min_sync_period` - Minimum period that IPVS rules are refreshed in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration.
65+
* `scheduler` - IPVS scheduler.
66+
* `tcp_timeout` - Timeout value used for idle IPVS TCP sessions in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration.
67+
* `tcp_fin_timeout` - Timeout value used for IPVS TCP sessions after receiving a FIN in RFC3339 duration.
68+
* `udp_timeout` - timeout value used for IPVS UDP packets in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration.

‎website/docs/r/cloud_project_kube.html.markdown

+80-38
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ resource "ovh_cloud_project_kube" "mycluster" {
3333
3434
resource "local_file" "kubeconfig" {
3535
content = ovh_cloud_project_kube.mycluster.kubeconfig
36-
filename = "mycluster.yml"
36+
filename = "mycluster.yml"
3737
}
3838
```
3939

@@ -94,34 +94,59 @@ Create a Kubernetes cluster in `GRA5` region with API Server AdmissionPlugins co
9494

9595
```hcl
9696
resource "ovh_cloud_project_kube" "mycluster" {
97-
service_name = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
98-
name = "my_kube_cluster"
99-
region = "GRA5"
100-
customization {
101-
apiserver {
102-
admissionplugins {
103-
enabled = ["NodeRestriction"]
104-
disabled = ["AlwaysPullImages"]
105-
}
106-
}
107-
}
97+
service_name = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
98+
name = "my_kube_cluster"
99+
region = "GRA5"
100+
customization_apiserver {
101+
admissionplugins {
102+
enabled = ["NodeRestriction"]
103+
disabled = ["AlwaysPullImages"]
104+
}
105+
}
106+
}
107+
```
108+
109+
Create a Kubernetes cluster in `GRA5` region with Kube proxy configuration, by specifying iptables or ipvs configurations:
110+
111+
```hcl
112+
resource "ovh_cloud_project_kube" "mycluster" {
113+
service_name = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
114+
name = "my_kube_cluster"
115+
region = "GRA5"
116+
kube_proxy_mode = "ipvs" # or "iptables"
117+
118+
customization_kube_proxy {
119+
iptables {
120+
min_sync_period = "PT0S"
121+
sync_period = "PT0S"
122+
}
123+
124+
ipvs {
125+
min_sync_period = "PT0S"
126+
sync_period = "PT0S"
127+
scheduler = "rr"
128+
tcp_timeout = "PT0S"
129+
tcp_fin_timeout = "PT0S"
130+
udp_timeout = "PT0S"
131+
}
132+
}
108133
}
109134
```
110135

111136
Kubernetes cluster creation attached to a VRack in `GRA5` region:
112137

113138
```hcl
114139
resource "ovh_vrack_cloudproject" "attach" {
115-
service_name = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # vrack ID
116-
project_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Public Cloud service name
140+
service_name = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # vrack ID
141+
project_id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # Public Cloud service name
117142
}
118143
119144
resource "ovh_cloud_project_network_private" "network" {
120-
service_name = ovh_vrack_cloudproject.attach.service_name
121-
vlan_id = 0
122-
name = "terraform_testacc_private_net"
123-
regions = ["GRA5"]
124-
depends_on = [ovh_vrack_cloudproject.attach]
145+
service_name = ovh_vrack_cloudproject.attach.service_name
146+
vlan_id = 0
147+
name = "terraform_testacc_private_net"
148+
regions = ["GRA5"]
149+
depends_on = [ovh_vrack_cloudproject.attach]
125150
}
126151
127152
resource "ovh_cloud_project_network_private_subnet" "networksubnet" {
@@ -140,24 +165,22 @@ resource "ovh_cloud_project_network_private_subnet" "networksubnet" {
140165
}
141166
142167
output "openstackID" {
143-
value = one(ovh_cloud_project_network_private.network.regions_attributes[*].openstackid)
168+
value = one(ovh_cloud_project_network_private.network.regions_attributes[*].openstackid)
144169
}
145170
146171
resource "ovh_cloud_project_kube" "mycluster" {
147-
service_name = var.service_name
148-
name = "test-kube-attach"
149-
region = "GRA5"
172+
service_name = var.service_name
173+
name = "test-kube-attach"
174+
region = "GRA5"
150175
151-
private_network_id = tolist(ovh_cloud_project_network_private.network.regions_attributes[*].openstackid)[0]
152-
153-
private_network_configuration {
154-
default_vrack_gateway = ""
155-
private_network_routing_as_default = false
156-
}
176+
private_network_id = tolist(ovh_cloud_project_network_private.network.regions_attributes[*].openstackid)[0]
157177
158-
depends_on = [
159-
ovh_cloud_project_network_private.network
160-
]
178+
private_network_configuration {
179+
default_vrack_gateway = ""
180+
private_network_routing_as_default = false
181+
}
182+
183+
depends_on = [ovh_cloud_project_network_private.network]
161184
}
162185
```
163186

@@ -169,12 +192,30 @@ The following arguments are supported:
169192
* `name` - (Optional) The name of the kubernetes cluster.
170193
* `region` - a valid OVHcloud public cloud region ID in which the kubernetes cluster will be available. Ex.: "GRA1". Defaults to all public cloud regions. Changing this value recreates the resource.
171194
* `version` - (Optional) kubernetes version to use. Changing this value updates the resource. Defaults to the latest available.
172-
* `customization` - (Optional) Customer customization object
195+
* `kube_proxy_mode` - (Optional) Selected mode for kube-proxy. **Changing this value recreates the resource. This will result in the loss of all data stored in the etcd.** Defaults to `iptables`.
196+
* `customization` - **Deprecated** (Optional) Use `customization_apiserver` and `customization_kube_proxy` instead. Kubernetes cluster customization
173197
* `apiserver` - Kubernetes API server customization
174-
* `admissionplugins` - (Optional) Kubernetes API server admission plugins customization
175-
* `enabled` - (Optional) Array of admission plugins enabled, default is ["NodeRestriction","AlwaysPulImages"] and only these admission plugins can be enabled at this time.
176-
* `disabled` - (Optional) Array of admission plugins disabled, default is [] and only AlwaysPulImages can be disabled at this time.
177-
* `private_network_id` - (Optional) OpenStack private network (or vrack) ID to use. Changing this value delete the resource (including ETCD user data). Defaults - not use private network. ~> __WARNING__ Updating the private network ID resets the cluster so that all user data is deleted.
198+
* `kube_proxy` - Kubernetes kube-proxy customization
199+
* `customization_apiserver` - Kubernetes API server customization
200+
* `admissionplugins` - (Optional) Kubernetes API server admission plugins customization
201+
* `enabled` - (Optional) Array of admission plugins enabled, default is ["NodeRestriction","AlwaysPulImages"] and only these admission plugins can be enabled at this time.
202+
* `disabled` - (Optional) Array of admission plugins disabled, default is [] and only AlwaysPulImages can be disabled at this time.
203+
* `customization_kube_proxy` - Kubernetes kube-proxy customization
204+
* `iptables` - (Optional) Kubernetes cluster kube-proxy customization of iptables specific config (durations format is RFC3339 duration, e.g. `PT60S`)
205+
* `sync_period` - (Optional) Minimum period that iptables rules are refreshed, in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration format (e.g. `PT60S`).
206+
* `min_sync_period` - (Optional) Period that iptables rules are refreshed, in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration format (e.g. `PT60S`). Must be greater than 0.
207+
* `ipvs` - (Optional) Kubernetes cluster kube-proxy customization of IPVS specific config (durations format is [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration, e.g. `PT60S`)
208+
* `sync_period` - (Optional) Minimum period that IPVS rules are refreshed, in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration format (e.g. `PT60S`).
209+
* `min_sync_period` - (Optional) Minimum period that IPVS rules are refreshed in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration (e.g. `PT60S`).
210+
* `scheduler` - (Optional) IPVS scheduler.
211+
* `tcp_timeout` - (Optional) Timeout value used for idle IPVS TCP sessions in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration (e.g. `PT60S`). The default value is `PT0S`, which preserves the current timeout value on the system.
212+
* `tcp_fin_timeout` - (Optional) Timeout value used for IPVS TCP sessions after receiving a FIN in RFC3339 duration (e.g. `PT60S`). The default value is `PT0S`, which preserves the current timeout value on the system.
213+
* `udp_timeout` - (Optional) timeout value used for IPVS UDP packets in [RFC3339](https://www.rfc-editor.org/rfc/rfc3339) duration (e.g. `PT60S`). The default value is `PT0S`, which preserves the current timeout value on the system.
214+
* `private_network_id` - (Optional) OpenStack private network (or vrack) ID to use.
215+
Changing this value delete the resource(including ETCD user data). Defaults - not use private network.
216+
217+
~> __WARNING__ Updating the private network ID resets the cluster so that all user data is deleted.
218+
178219
* `private_network_configuration` - (Optional) The private network configuration
179220
* `default_vrack_gateway` - If defined, all egress traffic will be routed towards this IP address, which should belong to the private network. Empty string means disabled.
180221
* `private_network_routing_as_default` - Defines whether routing should default to using the nodes' private interface, instead of their public interface. Default is false.
@@ -204,7 +245,8 @@ The following attributes are exported:
204245
* `update_policy` - See Argument Reference above.
205246
* `url` - Management URL of your cluster.
206247
* `version` - See Argument Reference above.
207-
* `customization` - See Argument Reference above.
248+
* `customization_apiserver` - See Argument Reference above.
249+
* `customization_kube_proxy` - See Argument Reference above.
208250

209251
## Timeouts
210252

0 commit comments

Comments
 (0)
Please sign in to comment.