Skip to content

Commit ae816d9

Browse files
authored
feat(k8s): create PN automatically with cluster if none given (#3152)
1 parent 3d77e65 commit ae816d9

24 files changed

Lines changed: 2622 additions & 4337 deletions

cmd/scw/testdata/test-all-usage-k8s-cluster-create-usage.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ ARGS:
5959
[open-id-connect-config.groups-prefix] Prefix prepended to group claims to prevent name collision (such as `system:` groups). For example, the value `oidc:` will create group names like `oidc:engineering` and `oidc:infra`
6060
[open-id-connect-config.required-claim.{index}] Multiple key=value pairs describing a required claim in the ID token. If set, the claims are verified to be present in the ID token with a matching value
6161
[apiserver-cert-sans.{index}] Additional Subject Alternative Names for the Kubernetes API server certificate
62-
[private-network-id] Private network ID for internal cluster communication (cannot be changed later)
62+
[private-network-id] Private network ID for internal cluster communication (cannot be changed later). For Kapsule clusters, if none is provided, a private network will be created
6363
[organization-id] Organization ID to use. If none is passed the default organization ID will be used
6464
[region=fr-par] Region to target. If none is passed will use default region from the config (fr-par | nl-ams | pl-waw)
6565

docs/commands/k8s.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ scw k8s cluster create [arg=value ...]
109109
| open-id-connect-config.groups-prefix | | Prefix prepended to group claims to prevent name collision (such as `system:` groups). For example, the value `oidc:` will create group names like `oidc:engineering` and `oidc:infra` |
110110
| open-id-connect-config.required-claim.{index} | | Multiple key=value pairs describing a required claim in the ID token. If set, the claims are verified to be present in the ID token with a matching value |
111111
| apiserver-cert-sans.{index} | | Additional Subject Alternative Names for the Kubernetes API server certificate |
112-
| private-network-id | | Private network ID for internal cluster communication (cannot be changed later) |
112+
| private-network-id | | Private network ID for internal cluster communication (cannot be changed later). For Kapsule clusters, if none is provided, a private network will be created |
113113
| organization-id | | Organization ID to use. If none is passed the default organization ID will be used |
114114
| region | Default: `fr-par`<br />One of: `fr-par`, `nl-ams`, `pl-waw` | Region to target. If none is passed will use default region from the config |
115115

internal/namespaces/k8s/v1/custom_cluster.go

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"fmt"
77
"net/http"
88
"reflect"
9+
"strings"
910
"time"
1011

1112
"github.com/fatih/color"
1213
"github.com/scaleway/scaleway-cli/v2/internal/core"
1314
"github.com/scaleway/scaleway-cli/v2/internal/human"
1415
k8s "github.com/scaleway/scaleway-sdk-go/api/k8s/v1"
16+
"github.com/scaleway/scaleway-sdk-go/api/vpc/v2"
1517
"github.com/scaleway/scaleway-sdk-go/scw"
1618
)
1719

@@ -88,6 +90,7 @@ func clusterCreateBuilder(c *core.Command) *core.Command {
8890

8991
c.ArgSpecs.GetByName("cni").Default = core.DefaultValueSetter("cilium")
9092
c.ArgSpecs.GetByName("version").Default = core.DefaultValueSetter("latest")
93+
c.ArgSpecs.GetByName("private-network-id").Short += ". For Kapsule clusters, if none is provided, a private network will be created"
9194

9295
c.Interceptor = func(ctx context.Context, argsI interface{}, runner core.CommandRunner) (interface{}, error) {
9396
args := argsI.(*k8s.CreateClusterRequest)
@@ -104,6 +107,84 @@ func clusterCreateBuilder(c *core.Command) *core.Command {
104107
return runner(ctx, args)
105108
}
106109

110+
c.Run = func(ctx context.Context, args interface{}) (i interface{}, e error) {
111+
request := args.(*k8s.CreateClusterRequest)
112+
113+
client := core.ExtractClient(ctx)
114+
k8sAPI := k8s.NewAPI(client)
115+
vpcAPI := vpc.NewAPI(client)
116+
117+
pnCreated := false
118+
var pn *vpc.PrivateNetwork
119+
var err error
120+
121+
if request.Type == "" || strings.HasPrefix(request.Type, "kapsule") {
122+
if request.PrivateNetworkID == nil {
123+
pn, err = vpcAPI.CreatePrivateNetwork(&vpc.CreatePrivateNetworkRequest{
124+
Region: request.Region,
125+
Tags: []string{"created-along-with-k8s-cluster", "created-by-cli"},
126+
}, scw.WithContext(ctx))
127+
if err != nil {
128+
return nil, err
129+
}
130+
request.PrivateNetworkID = scw.StringPtr(pn.ID)
131+
pnCreated = true
132+
} else {
133+
pn, err = vpcAPI.GetPrivateNetwork(&vpc.GetPrivateNetworkRequest{
134+
Region: request.Region,
135+
PrivateNetworkID: pn.ID,
136+
}, scw.WithContext(ctx))
137+
if err != nil {
138+
return nil, err
139+
}
140+
}
141+
}
142+
143+
cluster, err := k8sAPI.CreateCluster(request, scw.WithContext(ctx))
144+
if err != nil {
145+
if pnCreated {
146+
errPN := vpcAPI.DeletePrivateNetwork(&vpc.DeletePrivateNetworkRequest{
147+
Region: request.Region,
148+
PrivateNetworkID: pn.ID,
149+
}, scw.WithContext(ctx))
150+
151+
if err != nil {
152+
return nil, errors.Join(err, errPN)
153+
}
154+
}
155+
return nil, err
156+
}
157+
158+
return struct {
159+
*k8s.Cluster
160+
PrivateNetwork *vpc.PrivateNetwork `json:"private_network"`
161+
}{
162+
cluster,
163+
pn,
164+
}, nil
165+
}
166+
167+
c.View = &core.View{
168+
Sections: []*core.ViewSection{
169+
{
170+
FieldName: "AutoscalerConfig",
171+
Title: "Autoscaler configuration",
172+
},
173+
{
174+
FieldName: "AutoUpgrade",
175+
Title: "Auto-upgrade settings",
176+
},
177+
{
178+
FieldName: "OpenIDConnectConfig",
179+
Title: "Open ID Connect configuration",
180+
},
181+
{
182+
FieldName: "PrivateNetwork",
183+
Title: "Private Network",
184+
},
185+
},
186+
}
187+
107188
return c
108189
}
109190

@@ -204,9 +285,18 @@ func clusterUpdateBuilder(c *core.Command) *core.Command {
204285

205286
func waitForClusterFunc(action int) core.WaitFunc {
206287
return func(ctx context.Context, _, respI interface{}) (interface{}, error) {
288+
var clusterResponse *k8s.Cluster
289+
if action == clusterActionCreate {
290+
clusterResponse = respI.(struct {
291+
*k8s.Cluster
292+
PrivateNetwork *vpc.PrivateNetwork `json:"private_network"`
293+
}).Cluster
294+
} else {
295+
clusterResponse = respI.(*k8s.Cluster)
296+
}
207297
cluster, err := k8s.NewAPI(core.ExtractClient(ctx)).WaitForCluster(&k8s.WaitForClusterRequest{
208-
Region: respI.(*k8s.Cluster).Region,
209-
ClusterID: respI.(*k8s.Cluster).ID,
298+
Region: clusterResponse.Region,
299+
ClusterID: clusterResponse.ID,
210300
Timeout: scw.TimeDurationPtr(clusterActionTimeout),
211301
RetryInterval: core.DefaultRetryInterval,
212302
})
@@ -223,7 +313,7 @@ func waitForClusterFunc(action int) core.WaitFunc {
223313
notFoundError := &scw.ResourceNotFoundError{}
224314
responseError := &scw.ResponseError{}
225315
if errors.As(err, &responseError) && responseError.StatusCode == http.StatusNotFound || errors.As(err, &notFoundError) {
226-
return fmt.Sprintf("Cluster %s successfully deleted.", respI.(*k8s.Cluster).ID), nil
316+
return fmt.Sprintf("Cluster %s successfully deleted.", clusterResponse.ID), nil
227317
}
228318
}
229319
}

internal/namespaces/k8s/v1/custom_cluster_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
func Test_GetCluster(t *testing.T) {
1010
t.Run("Simple", core.Test(&core.TestConfig{
1111
Commands: GetCommands(),
12-
BeforeFunc: createCluster("Cluster", kapsuleVersion, 1, "DEV1-M"),
12+
BeforeFunc: createCluster("get-cluster", "Cluster", kapsuleVersion, 1, "DEV1-M"),
1313
Cmd: "scw k8s cluster get {{ .Cluster.ID }}",
1414
Check: core.TestCheckCombine(
1515
core.TestCheckGolden(),
@@ -22,7 +22,7 @@ func Test_GetCluster(t *testing.T) {
2222
func Test_WaitCluster(t *testing.T) {
2323
t.Run("wait for pools", core.Test(&core.TestConfig{
2424
Commands: GetCommands(),
25-
BeforeFunc: createCluster("Cluster", kapsuleVersion, 1, "GP1-XS"),
25+
BeforeFunc: createCluster("wait-cluster", "Cluster", kapsuleVersion, 1, "GP1-XS"),
2626
Cmd: "scw k8s cluster wait {{ .Cluster.ID }} wait-for-pools=true",
2727
Check: core.TestCheckCombine(
2828
core.TestCheckGolden(),

internal/namespaces/k8s/v1/custom_kubeconfig_get_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func Test_GetKubeconfig(t *testing.T) {
1515
////
1616
t.Run("simple", core.Test(&core.TestConfig{
1717
Commands: GetCommands(),
18-
BeforeFunc: createClusterAndWaitAndKubeconfig("Cluster", "Kubeconfig", kapsuleVersion),
18+
BeforeFunc: createClusterAndWaitAndKubeconfig("get-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
1919
Cmd: "scw k8s kubeconfig get {{ .Cluster.ID }}",
2020
Check: core.TestCheckCombine(
2121
core.TestCheckGolden(),

internal/namespaces/k8s/v1/custom_kubeconfig_install_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func Test_InstallKubeconfig(t *testing.T) {
141141
////
142142
t.Run("simple", core.Test(&core.TestConfig{
143143
Commands: GetCommands(),
144-
BeforeFunc: createClusterAndWaitAndKubeconfig("Cluster", "Kubeconfig", kapsuleVersion),
144+
BeforeFunc: createClusterAndWaitAndKubeconfig("install-kubeconfig-simple", "Cluster", "Kubeconfig", kapsuleVersion),
145145
Cmd: "scw k8s kubeconfig install {{ .Cluster.ID }}",
146146
Check: core.TestCheckCombine(
147147
// no golden tests since it's os specific
@@ -158,7 +158,7 @@ func Test_InstallKubeconfig(t *testing.T) {
158158

159159
t.Run("merge", core.Test(&core.TestConfig{
160160
Commands: GetCommands(),
161-
BeforeFunc: createClusterAndWaitAndKubeconfigAndPopulateFile("Cluster", "Kubeconfig", kapsuleVersion, path.Join(os.TempDir(), "cli-merge-test"), []byte(existingKubeconfig)),
161+
BeforeFunc: createClusterAndWaitAndKubeconfigAndPopulateFile("install-kubeconfig-merge", "Cluster", "Kubeconfig", kapsuleVersion, path.Join(os.TempDir(), "cli-merge-test"), []byte(existingKubeconfig)),
162162
Cmd: "scw k8s kubeconfig install {{ .Cluster.ID }}",
163163
Check: core.TestCheckCombine(
164164
// no golden tests since it's os specific

internal/namespaces/k8s/v1/custom_kubeconfig_uninstall_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
// testIfKubeconfigNotInFile checks if the given kubeconfig is not in the given file
16-
// it test if the user, cluster and context of the kubeconfig file are not in the given file
16+
// it tests if the user, cluster and context of the kubeconfig file are not in the given file
1717
func testIfKubeconfigNotInFile(t *testing.T, filePath string, suffix string, kubeconfig api.Config) {
1818
kubeconfigBytes, err := os.ReadFile(filePath)
1919
assert.Nil(t, err)
@@ -55,7 +55,7 @@ func Test_UninstallKubeconfig(t *testing.T) {
5555
////
5656
t.Run("uninstall", core.Test(&core.TestConfig{
5757
Commands: GetCommands(),
58-
BeforeFunc: createClusterAndWaitAndInstallKubeconfig("Cluster", "Kubeconfig", kapsuleVersion),
58+
BeforeFunc: createClusterAndWaitAndInstallKubeconfig("uninstall-kubeconfig", "Cluster", "Kubeconfig", kapsuleVersion),
5959
Cmd: "scw k8s kubeconfig uninstall {{ .Cluster.ID }}",
6060
Check: core.TestCheckCombine(
6161
// no golden tests since it's os specific
@@ -71,7 +71,7 @@ func Test_UninstallKubeconfig(t *testing.T) {
7171
}))
7272
t.Run("empty file", core.Test(&core.TestConfig{
7373
Commands: GetCommands(),
74-
BeforeFunc: createClusterAndWaitAndKubeconfig("EmptyCluster", "Kubeconfig", kapsuleVersion),
74+
BeforeFunc: createClusterAndWaitAndKubeconfig("uninstall-kubeconfig-empty", "EmptyCluster", "Kubeconfig", kapsuleVersion),
7575
Cmd: "scw k8s kubeconfig uninstall {{ .EmptyCluster.ID }}",
7676
Check: core.TestCheckCombine(
7777
// no golden tests since it's os specific
@@ -88,7 +88,7 @@ func Test_UninstallKubeconfig(t *testing.T) {
8888
}))
8989
t.Run("uninstall-merge", core.Test(&core.TestConfig{
9090
Commands: GetCommands(),
91-
BeforeFunc: createClusterAndWaitAndKubeconfigAndPopulateFileAndInstall("Cluster", "Kubeconfig", kapsuleVersion, path.Join(os.TempDir(), "cli-uninstall-merge-test"), []byte(existingKubeconfig)),
91+
BeforeFunc: createClusterAndWaitAndKubeconfigAndPopulateFileAndInstall("uninstall-kubeconfig-merge", "Cluster", "Kubeconfig", kapsuleVersion, path.Join(os.TempDir(), "cli-uninstall-merge-test"), []byte(existingKubeconfig)),
9292
Cmd: "scw k8s kubeconfig uninstall {{ .Cluster.ID }}",
9393
Check: core.TestCheckCombine(
9494
// no golden tests since it's os specific

internal/namespaces/k8s/v1/custom_version_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ func Test_GetVersion(t *testing.T) {
1212
////
1313
t.Run("simple", core.Test(&core.TestConfig{
1414
Commands: GetCommands(),
15-
Cmd: "scw k8s version get 1.20.2",
15+
Cmd: "scw k8s version get " + kapsuleVersion,
1616
Check: core.TestCheckCombine(
1717
core.TestCheckGolden(),
1818
core.TestCheckExitCode(0),

internal/namespaces/k8s/v1/helpers_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
)
1313

1414
const (
15-
kapsuleVersion = "1.17.3"
15+
kapsuleVersion = "1.27.1"
1616
)
1717

1818
//
@@ -21,17 +21,17 @@ const (
2121

2222
// createCluster creates a basic cluster with "poolSize" dev1-m as nodes, the given version and
2323
// register it in the context Meta at metaKey.
24-
func createCluster(metaKey string, version string, poolSize int, nodeType string) core.BeforeFunc {
24+
func createCluster(clusterNameSuffix string, metaKey string, version string, poolSize int, nodeType string) core.BeforeFunc {
2525
return core.ExecStoreBeforeCmd(
2626
metaKey,
27-
fmt.Sprintf("scw k8s cluster create name=cli-test version=%s cni=cilium pools.0.node-type=%s pools.0.size=%d pools.0.name=default", version, nodeType, poolSize))
27+
fmt.Sprintf("scw k8s cluster create name=cli-test-%s version=%s cni=cilium pools.0.node-type=%s pools.0.size=%d pools.0.name=default", clusterNameSuffix, version, nodeType, poolSize))
2828
}
2929

3030
// createClusterAndWaitAndKubeconfig creates a basic cluster with 1 dev1-m as node, the given version and
3131
// register it in the context Meta at metaKey.
32-
func createClusterAndWaitAndKubeconfig(metaKey string, kubeconfigMetaKey string, version string) core.BeforeFunc {
32+
func createClusterAndWaitAndKubeconfig(clusterNameSuffix string, metaKey string, kubeconfigMetaKey string, version string) core.BeforeFunc {
3333
return func(ctx *core.BeforeFuncCtx) error {
34-
cmd := fmt.Sprintf("scw k8s cluster create name=cli-test version=%s cni=cilium pools.0.node-type=DEV1-M pools.0.size=1 pools.0.name=default --wait", version)
34+
cmd := fmt.Sprintf("scw k8s cluster create name=cli-test-%s version=%s cni=cilium pools.0.node-type=DEV1-M pools.0.size=1 pools.0.name=default --wait", clusterNameSuffix, version)
3535
res := ctx.ExecuteCmd(strings.Split(cmd, " "))
3636
cluster := res.(*k8s.Cluster)
3737
ctx.Meta[metaKey] = cluster
@@ -58,9 +58,9 @@ func createClusterAndWaitAndKubeconfig(metaKey string, kubeconfigMetaKey string,
5858

5959
// createClusterAndWaitAndInstallKubeconfig creates a basic cluster with 1 dev1-m as node, the given version and
6060
// register it in the context Meta at metaKey. And install the kubeconfig
61-
func createClusterAndWaitAndInstallKubeconfig(metaKey string, kubeconfigMetaKey string, version string) core.BeforeFunc {
61+
func createClusterAndWaitAndInstallKubeconfig(clusterNameSuffix string, metaKey string, kubeconfigMetaKey string, version string) core.BeforeFunc {
6262
return func(ctx *core.BeforeFuncCtx) error {
63-
cmd := fmt.Sprintf("scw k8s cluster create name=cli-test version=%s cni=cilium pools.0.node-type=DEV1-M pools.0.size=1 pools.0.name=default --wait", version)
63+
cmd := fmt.Sprintf("scw k8s cluster create name=cli-test-%s version=%s cni=cilium pools.0.node-type=DEV1-M pools.0.size=1 pools.0.name=default --wait", clusterNameSuffix, version)
6464
res := ctx.ExecuteCmd(strings.Split(cmd, " "))
6565
cluster := res.(*k8s.Cluster)
6666
ctx.Meta[metaKey] = cluster
@@ -89,9 +89,9 @@ func createClusterAndWaitAndInstallKubeconfig(metaKey string, kubeconfigMetaKey
8989

9090
// createClusterAndWaitAndKubeconfigAndPopulateFile creates a basic cluster with 1 dev1-m as node, the given version and
9191
// register it in the context Meta at metaKey. It also populates the given file with the given content
92-
func createClusterAndWaitAndKubeconfigAndPopulateFile(metaKey string, kubeconfigMetaKey string, version string, file string, content []byte) core.BeforeFunc {
92+
func createClusterAndWaitAndKubeconfigAndPopulateFile(clusterNameSuffix string, metaKey string, kubeconfigMetaKey string, version string, file string, content []byte) core.BeforeFunc {
9393
return func(ctx *core.BeforeFuncCtx) error {
94-
cmd := fmt.Sprintf("scw k8s cluster create name=cli-test version=%s cni=cilium pools.0.node-type=DEV1-M pools.0.size=1 pools.0.name=default --wait", version)
94+
cmd := fmt.Sprintf("scw k8s cluster create name=cli-test-%s version=%s cni=cilium pools.0.node-type=DEV1-M pools.0.size=1 pools.0.name=default --wait", clusterNameSuffix, version)
9595
res := ctx.ExecuteCmd(strings.Split(cmd, " "))
9696
cluster := res.(*k8s.Cluster)
9797
ctx.Meta[metaKey] = cluster
@@ -119,9 +119,9 @@ func createClusterAndWaitAndKubeconfigAndPopulateFile(metaKey string, kubeconfig
119119

120120
// createClusterAndWaitAndKubeconfigAndPopulateFileAndInstall creates a basic cluster with 1 dev1-m as node, the given version and
121121
// register it in the context Meta at metaKey. It also populates the given file with the given content and install the new kubeconfig
122-
func createClusterAndWaitAndKubeconfigAndPopulateFileAndInstall(metaKey string, kubeconfigMetaKey string, version string, file string, content []byte) core.BeforeFunc {
122+
func createClusterAndWaitAndKubeconfigAndPopulateFileAndInstall(clusterNameSuffix string, metaKey string, kubeconfigMetaKey string, version string, file string, content []byte) core.BeforeFunc {
123123
return func(ctx *core.BeforeFuncCtx) error {
124-
cmd := fmt.Sprintf("scw k8s cluster create name=cli-test version=%s cni=cilium pools.0.node-type=DEV1-M pools.0.size=1 pools.0.name=default --wait", version)
124+
cmd := fmt.Sprintf("scw k8s cluster create name=cli-test-%s version=%s cni=cilium pools.0.node-type=DEV1-M pools.0.size=1 pools.0.name=default --wait", clusterNameSuffix, version)
125125
res := ctx.ExecuteCmd(strings.Split(cmd, " "))
126126
cluster := res.(*k8s.Cluster)
127127
ctx.Meta[metaKey] = cluster

0 commit comments

Comments
 (0)