Skip to content

Commit ec08774

Browse files
authored
feat(k8s): add option to wait for both cluster and node-groups in create (#567)
1 parent 912b7d7 commit ec08774

File tree

5 files changed

+156
-4
lines changed

5 files changed

+156
-4
lines changed

.github/workflows/examples.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ jobs:
3232
go install github.com/UpCloudLtd/mdtest@latest
3333
make build
3434
echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
35+
- name: Install kubectl
36+
uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede
37+
with:
38+
version: 'latest'
3539
- name: Test examples
3640
env:
3741
UPCLOUD_USERNAME: ${{ secrets.UPCLOUD_USERNAME }}

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- In `kubernetes create`, allow waiting for cluster and its node-groups to reach running state with `--wait=all` flag. When using `--wait` or `--wait=cluster`, the command will wait only for the cluster to reach running state.
13+
1014
## [3.26.0] - 2025-11-26
1115

1216
### Added
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Create a Kubernetes cluster and expose a service
2+
3+
This example demonstrates how to create a Kubernetes cluster with `upctl`, create a deployment, and expose it to the internet using a service.
4+
5+
To keep track of resources created during this example, we will use common prefix in all resource names. The variable definitions also include configuration for the cluster, node-group, and service. By default, we will use `NodePort` service type (as it is faster to deploy), but you can change `svc_type` value to `LoadBalancer` if you want to use UpCloud Load Balancer to expose the service.
6+
7+
```env
8+
prefix=example-upctl-kubernetes-
9+
zone=pl-waw1
10+
11+
# Network
12+
cidr=172.30.100.0/24
13+
14+
# Cluster
15+
plan=dev-md
16+
17+
# Node-group
18+
ng_count=1
19+
ng_name=default
20+
ng_plan=2xCPU-4GB
21+
test_label=upctl-example
22+
23+
# Service
24+
svc_type=NodePort
25+
26+
KUBECONFIG=./kubeconfig.yaml
27+
```
28+
29+
First, we will need a private network for the cluster.
30+
31+
```sh
32+
upctl network create \
33+
--name ${prefix}net \
34+
--zone $zone \
35+
--ip-network address=$cidr,dhcp=true;
36+
```
37+
38+
Next, we can create the Kubernetes cluster.
39+
40+
```sh
41+
upctl kubernetes create \
42+
--name ${prefix}cluster \
43+
--network ${prefix}net \
44+
--plan $plan \
45+
--zone $zone \
46+
--kubernetes-api-allow-ip "0.0.0.0/0" \
47+
--node-group count=$ng_count,name=$ng_name,plan=$ng_plan,label="test=$test_label" \
48+
--wait=all;
49+
```
50+
51+
Once the cluster is created, we can get the kubeconfig file to interact with the cluster using `kubectl`.
52+
53+
```sh
54+
upctl kubernetes config ${prefix}cluster \
55+
--write $KUBECONFIG;
56+
```
57+
58+
Now we can create a deployment and expose it using a service.
59+
60+
```sh
61+
kubectl create deployment --image=ghcr.io/upcloudltd/hello hello-uks
62+
kubectl expose deployment hello-uks --port=80 --target-port=80 --type=$svc_type
63+
```
64+
65+
If using `NodePort` service type, we need to get the node IP and service port to access the service.
66+
67+
```sh when='svc_type == "NodePort"'
68+
# Get node IP
69+
node_ip=$(kubectl get node -o json | jq -r '.items[0].status.addresses.[] | select(.type == "ExternalIP").address')
70+
# Get service port
71+
svc_port=$(kubectl get service hello-uks -o json | jq -r '.spec.ports[0].nodePort')
72+
73+
# Wait until the service is reachable
74+
until curl -sSf $node_ip:$svc_port; do
75+
sleep 15;
76+
done;
77+
```
78+
79+
If using `LoadBalancer` service type, we need to wait until the load balancer has been created and assigned a public hostname. Once the hostname is available, we can try to access the service.
80+
81+
```sh when='svc_type == "LoadBalancer"'
82+
# Wait for hostname to be available in service status
83+
until kubectl get service hello-uks -o json | jq -re .status.loadBalancer.ingress[0].hostname; do
84+
sleep 15;
85+
done;
86+
87+
# Wait until the service is reachable
88+
hostname=$(kubectl get service hello-uks -o json | jq -re .status.loadBalancer.ingress[0].hostname)
89+
until curl -sSf $hostname; do
90+
sleep 15;
91+
done;
92+
```
93+
94+
Finally, we can clean up the created resources.
95+
96+
```sh cleanup
97+
kubectl delete service hello-uks
98+
99+
upctl all purge --include "*${prefix}*";
100+
```

internal/commands/kubernetes/create.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type createParams struct {
5151
networkArg string
5252
nodeGroups []string
5353
privateNodeGroups config.OptionalBoolean
54-
wait config.OptionalBoolean
54+
wait string
5555
}
5656

5757
func (p *createParams) processParams(exec commands.Executor) error {
@@ -154,7 +154,8 @@ func (c *createCommand) InitCommand() {
154154
)
155155
config.AddToggleFlag(fs, &c.params.privateNodeGroups, "private-node-groups", false, "Do not assign public IPs to worker nodes. If set, the attached network should have a NAT gateway configured to provide internet access to the worker nodes.")
156156
fs.StringVar(&c.params.Zone, "zone", "", namedargs.ZoneDescription("cluster"))
157-
config.AddToggleFlag(fs, &c.params.wait, "wait", false, "Wait for cluster to be in running state before returning.")
157+
fs.StringVar(&c.params.wait, "wait", "none", "Wait for resources to be in running state before returning. Use `--wait` to wait for cluster and `--wait=all` to also wait for all node groups to be in running state.")
158+
fs.Lookup("wait").NoOptDefVal = "cluster"
158159
c.AddFlags(fs)
159160

160161
commands.Must(c.Cobra().MarkFlagRequired("name"))
@@ -195,9 +196,12 @@ func (c *createCommand) ExecuteWithoutArguments(exec commands.Executor) (output.
195196
return commands.HandleError(exec, msg, err)
196197
}
197198

198-
if c.params.wait.Value() {
199+
switch c.params.wait {
200+
case "cluster":
199201
WaitForClusterState(res.UUID, upcloud.KubernetesClusterStateRunning, exec, msg)
200-
} else {
202+
case "all":
203+
waitUntilClusterAndNodeGroupsRunning(res.UUID, exec, msg)
204+
default:
201205
exec.PushProgressSuccess(msg)
202206
}
203207

internal/commands/kubernetes/wait.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,43 @@ func WaitForClusterState(uuid string, state upcloud.KubernetesClusterState, exec
3030
exec.PushProgressUpdateMessage(msg, msg)
3131
exec.PushProgressSuccess(msg)
3232
}
33+
34+
func waitUntilClusterAndNodeGroupsRunning(uuid string, exec commands.Executor, msg string) {
35+
ctx := exec.Context()
36+
37+
exec.PushProgressUpdateMessage(msg, fmt.Sprintf("Waiting for cluster %s to be in running state", uuid))
38+
39+
cluster, err := exec.All().WaitForKubernetesClusterState(ctx, &request.WaitForKubernetesClusterStateRequest{
40+
UUID: uuid,
41+
DesiredState: upcloud.KubernetesClusterStateRunning,
42+
})
43+
if err != nil {
44+
exec.PushProgressUpdate(messages.Update{
45+
Key: msg,
46+
Message: msg,
47+
Status: messages.MessageStatusWarning,
48+
Details: "Error: " + err.Error(),
49+
})
50+
return
51+
}
52+
53+
exec.PushProgressUpdateMessage(msg, fmt.Sprintf("Waiting for cluster %s node-groups to be in running state", uuid))
54+
for _, ng := range cluster.NodeGroups {
55+
if _, err := exec.All().WaitForKubernetesNodeGroupState(ctx, &request.WaitForKubernetesNodeGroupStateRequest{
56+
ClusterUUID: uuid,
57+
Name: ng.Name,
58+
DesiredState: upcloud.KubernetesNodeGroupStateRunning,
59+
}); err != nil {
60+
exec.PushProgressUpdate(messages.Update{
61+
Key: msg,
62+
Message: msg,
63+
Status: messages.MessageStatusWarning,
64+
Details: "Error: " + err.Error(),
65+
})
66+
return
67+
}
68+
}
69+
70+
exec.PushProgressUpdateMessage(msg, msg)
71+
exec.PushProgressSuccess(msg)
72+
}

0 commit comments

Comments
 (0)