diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 97a8745..7c0b26d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -192,9 +192,6 @@ jobs: go-version: "1.25" cache: true - - name: Install dependencies - run: sudo apt-get update && sudo apt-get install -y jq sshpass - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.12.0 diff --git a/AGENTS.md b/AGENTS.md index fe5595e..1c8b253 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,10 +5,14 @@ - Don't include comments unless they add meaningful context. - Use the `errors` package for sentinel errors. - Don't ignore errors by assigning to `_`. +- Ensure any defaults are the same in deploy/ and charts/ ## Testing instructions -- Fix any test or type errors until everything succeeds. - Add or update tests for the code you change, even if nobody asked. -- Update Go tests and scripts/e2e-tests.sh as needed. +- Tests are defined in: + - `charts/binarylane-cloud-controller-manager/tests` + - `scripts/e2e-tests.sh` + - `internal/**/*_test.go` - Run deploy-cluster.sh before e2e-tests.sh, and then delete-cluster.sh after. - Run golangci-lint locally to ensure no linting errors. +- Before finishing, run tests and ensure they pass. diff --git a/README.md b/README.md index 232b06c..ca73a8e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ kubectl create secret generic binarylane-api-token \ # Install the chart from GitHub Container Registry helm install binarylane-ccm \ oci://ghcr.io/oscarhermoso/charts/binarylane-cloud-controller-manager \ - --version 0.2.2 \ + --version 0.2.4 \ --namespace kube-system \ --set cloudControllerManager.secret.name="binarylane-api-token" ``` @@ -60,13 +60,13 @@ kubectl create secret generic binarylane-api-token \ 2. **Deploy the RBAC configuration:** ```bash -kubectl apply -f https://raw.githubusercontent.com/oscarhermoso/binarylane-cloud-controller-manager/main/deploy/kubernetes/rbac.yaml +kubectl apply -f https://raw.githubusercontent.com/oscarhermoso/binarylane-cloud-controller-manager/v0.2.4/deploy/kubernetes/rbac.yaml ``` 3. **Deploy the cloud controller manager:** ```bash -kubectl apply -f https://raw.githubusercontent.com/oscarhermoso/binarylane-cloud-controller-manager/main/deploy/kubernetes/deployment.yaml +kubectl apply -f https://raw.githubusercontent.com/oscarhermoso/binarylane-cloud-controller-manager/v0.2.4/deploy/kubernetes/deployment.yaml ``` ## Configuration diff --git a/charts/binarylane-cloud-controller-manager/Chart.yaml b/charts/binarylane-cloud-controller-manager/Chart.yaml index 6643df2..7d93062 100644 --- a/charts/binarylane-cloud-controller-manager/Chart.yaml +++ b/charts/binarylane-cloud-controller-manager/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: binarylane-cloud-controller-manager description: Kubernetes Cloud Controller Manager for BinaryLane type: application -version: 0.2.3 -appVersion: '0.2.3' +version: 0.2.4 +appVersion: '0.2.4' keywords: - kubernetes - cloud-controller-manager diff --git a/charts/binarylane-cloud-controller-manager/README.md b/charts/binarylane-cloud-controller-manager/README.md index 4c15e6b..4385875 100644 --- a/charts/binarylane-cloud-controller-manager/README.md +++ b/charts/binarylane-cloud-controller-manager/README.md @@ -16,9 +16,10 @@ The following table lists the configurable parameters of the chart and their def | `image.repository` | Image repository | `ghcr.io/oscarhermoso/binarylane-cloud-controller-manager` | | `image.pullPolicy` | Image pull policy | `IfNotPresent` | | `image.tag` | Image tag | Chart appVersion | -| `cloudControllerManager.secret.name` | Name of secret containing API token | `""` | +| `cloudControllerManager.secret.name` | Name of secret containing API token | `"binarylane-api-token"` | | `cloudControllerManager.secret.key` | Key in secret for API token | `api-token` | | `serviceAccount.create` | Create service account | `true` | +| `serviceAccount.automount` | Automount SA token | `true` | | `serviceAccount.name` | Service account name | Generated from template | | `resources.limits.cpu` | CPU limit | `200m` | | `resources.limits.memory` | Memory limit | `128Mi` | diff --git a/charts/binarylane-cloud-controller-manager/templates/deployment.yaml b/charts/binarylane-cloud-controller-manager/templates/deployment.yaml index 699c2aa..607411f 100644 --- a/charts/binarylane-cloud-controller-manager/templates/deployment.yaml +++ b/charts/binarylane-cloud-controller-manager/templates/deployment.yaml @@ -41,13 +41,14 @@ spec: securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - - name: cloud-controller-manager + - name: binarylane-cloud-controller-manager securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} command: - /binarylane-cloud-controller-manager + - --allow-untagged-cloud - --cloud-provider=binarylane - --leader-elect=true - --use-service-account-credentials=true @@ -66,14 +67,24 @@ spec: {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.extraVolumeMounts }} volumeMounts: + - mountPath: /etc/kubernetes/ + name: k8s + - mountPath: /etc/kubernetes/config + name: cloud-config + {{- with .Values.extraVolumeMounts }} {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.extraVolumes }} + {{- end }} volumes: + - name: k8s + hostPath: + path: /etc/kubernetes + - name: cloud-config + hostPath: + path: /etc/kubernetes/config + {{- with .Values.extraVolumes }} {{- toYaml . | nindent 8 }} - {{- end }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/binarylane-cloud-controller-manager/tests/deployment_test.yaml b/charts/binarylane-cloud-controller-manager/tests/deployment_test.yaml index c84b2f8..cf023d8 100644 --- a/charts/binarylane-cloud-controller-manager/tests/deployment_test.yaml +++ b/charts/binarylane-cloud-controller-manager/tests/deployment_test.yaml @@ -2,6 +2,7 @@ suite: test deployment templates: - deployment.yaml - rbac.yaml + - serviceaccount.yaml - test-secret.yaml tests: - it: should create a deployment with correct replicas @@ -191,3 +192,80 @@ tests: - equal: path: spec.template.spec.containers[0].resources.requests.cpu value: '200m' + + - it: should set automountServiceAccountToken by default + template: serviceaccount.yaml + set: + cloudControllerManager.secret.name: 'binarylane-api-token' + asserts: + - equal: + path: automountServiceAccountToken + value: true + + - it: should set automountServiceAccountToken when configured + template: serviceaccount.yaml + set: + cloudControllerManager.secret.name: 'binarylane-api-token' + serviceAccount.automount: false + asserts: + - equal: + path: automountServiceAccountToken + value: false + + - it: should use correct container name + template: deployment.yaml + set: + cloudControllerManager.secret.name: 'binarylane-api-token' + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: binarylane-cloud-controller-manager + + - it: should include --allow-untagged-cloud flag + template: deployment.yaml + set: + cloudControllerManager.secret.name: 'binarylane-api-token' + asserts: + - contains: + path: spec.template.spec.containers[0].command + content: --allow-untagged-cloud + + - it: should mount kubernetes config volumes + template: deployment.yaml + set: + cloudControllerManager.secret.name: 'binarylane-api-token' + asserts: + - contains: + path: spec.template.spec.volumes + content: + name: k8s + hostPath: + path: /etc/kubernetes + - contains: + path: spec.template.spec.volumes + content: + name: cloud-config + hostPath: + path: /etc/kubernetes/config + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + mountPath: /etc/kubernetes/ + name: k8s + - contains: + path: spec.template.spec.containers[0].volumeMounts + content: + mountPath: /etc/kubernetes/config + name: cloud-config + + - it: should use default secret name + template: deployment.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BINARYLANE_API_TOKEN + valueFrom: + secretKeyRef: + name: binarylane-api-token + key: api-token diff --git a/charts/binarylane-cloud-controller-manager/values-example.yaml b/charts/binarylane-cloud-controller-manager/values-example.yaml index f18e703..bb0da3f 100644 --- a/charts/binarylane-cloud-controller-manager/values-example.yaml +++ b/charts/binarylane-cloud-controller-manager/values-example.yaml @@ -60,6 +60,8 @@ nodeSelector: # Tolerations - allow scheduling on control plane nodes tolerations: + - key: CriticalAddonsOnly + operator: Exists - effect: NoSchedule key: node-role.kubernetes.io/control-plane operator: Exists diff --git a/charts/binarylane-cloud-controller-manager/values.yaml b/charts/binarylane-cloud-controller-manager/values.yaml index 6a00722..ef0a171 100644 --- a/charts/binarylane-cloud-controller-manager/values.yaml +++ b/charts/binarylane-cloud-controller-manager/values.yaml @@ -21,7 +21,7 @@ cloudControllerManager: # Name of secret containing BinaryLane API token # Create: kubectl create secret generic binarylane-api-token --from-literal=api-token="YOUR_TOKEN" -n kube-system secret: - name: '' + name: 'binarylane-api-token' key: 'api-token' serviceAccount: @@ -53,12 +53,12 @@ securityContext: readOnlyRootFilesystem: true resources: - limits: - cpu: 200m - memory: 128Mi requests: cpu: 100m memory: 64Mi + limits: + cpu: 500m + memory: 1Gi # Node selector for pod assignment nodeSelector: @@ -66,6 +66,9 @@ nodeSelector: # Tolerations for pod assignment tolerations: + # Tolerate CriticalAddonsOnly taint + - key: CriticalAddonsOnly + operator: Exists # Tolerate control-plane taint (Kubernetes 1.24+) - effect: NoSchedule key: node-role.kubernetes.io/control-plane diff --git a/deploy/kubernetes/deployment.yaml b/deploy/kubernetes/deployment.yaml index f6834ae..62d07e2 100644 --- a/deploy/kubernetes/deployment.yaml +++ b/deploy/kubernetes/deployment.yaml @@ -5,27 +5,37 @@ metadata: namespace: kube-system labels: app.kubernetes.io/name: binarylane-cloud-controller-manager + app.kubernetes.io/instance: binarylane-cloud-controller-manager + app.kubernetes.io/version: '0.2.4' app.kubernetes.io/component: cloud-controller-manager app.kubernetes.io/part-of: kubernetes spec: replicas: 1 # Adjust the number of replicas as needed - revisionHistoryLimit: 2 strategy: type: Recreate selector: matchLabels: app.kubernetes.io/name: binarylane-cloud-controller-manager + app.kubernetes.io/instance: binarylane-cloud-controller-manager template: metadata: labels: app.kubernetes.io/name: binarylane-cloud-controller-manager + app.kubernetes.io/instance: binarylane-cloud-controller-manager + app.kubernetes.io/version: '0.2.4' app.kubernetes.io/component: cloud-controller-manager app.kubernetes.io/part-of: kubernetes spec: dnsPolicy: Default hostNetwork: true - serviceAccountName: cloud-controller-manager + serviceAccountName: binarylane-cloud-controller-manager priorityClassName: system-cluster-critical + securityContext: + runAsNonRoot: true + runAsUser: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: @@ -33,7 +43,10 @@ spec: labelSelector: matchLabels: app.kubernetes.io/name: binarylane-cloud-controller-manager + app.kubernetes.io/instance: binarylane-cloud-controller-manager tolerations: + - key: CriticalAddonsOnly + operator: Exists - effect: NoSchedule key: node-role.kubernetes.io/control-plane operator: Exists @@ -54,16 +67,25 @@ spec: - effect: NoSchedule key: node.kubernetes.io/not-ready operator: Exists + nodeSelector: + node-role.kubernetes.io/control-plane: '' containers: - name: binarylane-cloud-controller-manager - image: ghcr.io/oscarhermoso/binarylane-cloud-controller-manager:latest + image: ghcr.io/oscarhermoso/binarylane-cloud-controller-manager:0.2.4 imagePullPolicy: IfNotPresent + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true command: - - '/binarylane-cloud-controller-manager' - - '--cloud-provider=binarylane' - - '--leader-elect=true' - - '--use-service-account-credentials=true' - - '--v=2' + - /binarylane-cloud-controller-manager + - --allow-untagged-cloud + - --cloud-provider=binarylane + - --leader-elect=true + - --use-service-account-credentials=true + - --v=2 env: - name: BINARYLANE_API_TOKEN valueFrom: @@ -73,7 +95,19 @@ spec: resources: requests: cpu: 100m - memory: 50Mi + memory: 64Mi limits: - cpu: 200m - memory: 100Mi + cpu: 500m + memory: 1Gi + volumeMounts: + - mountPath: /etc/kubernetes/ + name: k8s + - mountPath: /etc/kubernetes/config + name: cloud-config + volumes: + - name: k8s + hostPath: + path: /etc/kubernetes + - name: cloud-config + hostPath: + path: /etc/kubernetes/config diff --git a/deploy/kubernetes/rbac.yaml b/deploy/kubernetes/rbac.yaml index 82c3a9b..b27e357 100644 --- a/deploy/kubernetes/rbac.yaml +++ b/deploy/kubernetes/rbac.yaml @@ -1,19 +1,23 @@ apiVersion: v1 kind: ServiceAccount metadata: - name: cloud-controller-manager + name: binarylane-cloud-controller-manager namespace: kube-system labels: app.kubernetes.io/name: binarylane-cloud-controller-manager + app.kubernetes.io/instance: binarylane-cloud-controller-manager + app.kubernetes.io/version: '0.2.4' app.kubernetes.io/component: cloud-controller-manager app.kubernetes.io/part-of: kubernetes --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: system:cloud-controller-manager + name: binarylane-cloud-controller-manager labels: app.kubernetes.io/name: binarylane-cloud-controller-manager + app.kubernetes.io/instance: binarylane-cloud-controller-manager + app.kubernetes.io/version: '0.2.4' app.kubernetes.io/component: cloud-controller-manager app.kubernetes.io/part-of: kubernetes rules: @@ -30,31 +34,35 @@ rules: resources: - nodes verbs: - - '*' + - get + - list + - watch + - update + - patch - apiGroups: - '' resources: - nodes/status verbs: - patch + - update - apiGroups: - '' resources: - services verbs: + - get - list + - watch - patch - update - - watch - apiGroups: - '' resources: - services/status verbs: - - list - patch - update - - watch - apiGroups: - '' resources: @@ -62,6 +70,15 @@ rules: verbs: - create - get + - list + - watch + - update + - apiGroups: + - '' + resources: + - serviceaccounts/token + verbs: + - create - apiGroups: - '' resources: @@ -69,8 +86,8 @@ rules: verbs: - get - list - - update - watch + - update - apiGroups: - '' resources: @@ -86,15 +103,21 @@ rules: resources: - leases verbs: + - get - create + - update + - apiGroups: + - '' + resources: + - secrets + verbs: - get - list - watch - - update - apiGroups: - '' resources: - - secrets + - configmaps verbs: - get - list @@ -103,16 +126,18 @@ rules: kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: system:cloud-controller-manager + name: binarylane-cloud-controller-manager labels: app.kubernetes.io/name: binarylane-cloud-controller-manager + app.kubernetes.io/instance: binarylane-cloud-controller-manager + app.kubernetes.io/version: '0.2.4' app.kubernetes.io/component: cloud-controller-manager app.kubernetes.io/part-of: kubernetes roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole - name: system:cloud-controller-manager + name: binarylane-cloud-controller-manager subjects: - kind: ServiceAccount - name: cloud-controller-manager + name: binarylane-cloud-controller-manager namespace: kube-system diff --git a/deploy/kubernetes/secret.yaml b/deploy/kubernetes/secret.yaml index 17d99fd..fa79f95 100644 --- a/deploy/kubernetes/secret.yaml +++ b/deploy/kubernetes/secret.yaml @@ -5,6 +5,8 @@ metadata: namespace: kube-system labels: app.kubernetes.io/name: binarylane-cloud-controller-manager + app.kubernetes.io/instance: binarylane-cloud-controller-manager + app.kubernetes.io/version: '0.2.4' app.kubernetes.io/component: cloud-controller-manager app.kubernetes.io/part-of: kubernetes type: Opaque diff --git a/internal/binarylane/types_gen.go b/internal/binarylane/types_gen.go index c80f3d3..ade4d4f 100644 --- a/internal/binarylane/types_gen.go +++ b/internal/binarylane/types_gen.go @@ -1141,7 +1141,7 @@ type ChangeSizeOptionsRequest struct { // If specified this is the absolute value, not just the additional transfer above what is included in the size. // Leave null to accept the default for the size if this is a new server or a resize to a different base size, or to keep the current value if this a resize with the same base size but different options. // - // Valid values (when converted to GB by multiplying the value provided by 1024): + // Valid values (when converted to GB by multiplying the value provided by 1000): // - must be a multiple of 5GB // - > 30GB must be a multiple of 10 // - > 200GB must be a multiple of 100 @@ -2877,7 +2877,7 @@ type SizeOptions struct { // TransferCostPerAdditionalGigabyte The additional cost per GB per month for additional included transfer. TransferCostPerAdditionalGigabyte float64 `json:"transfer_cost_per_additional_gigabyte"` - // TransferMax The maximum transfer in TB permitted for this size. + // TransferMax The maximum transfer in TB permitted for this size. If this is the same as Size.Transfer no additional transfer is supported. TransferMax float64 `json:"transfer_max"` // WeeklyBackups The number of weekly backups included in the base size cost. @@ -2923,7 +2923,7 @@ type SizeOptionsRequest struct { // If specified this is the absolute value, not just the additional transfer above what is included in the size. // Leave null to accept the default for the size if this is a new server or a resize to a different base size, or to keep the current value if this a resize with the same base size but different options. // - // Valid values (when converted to GB by multiplying the value provided by 1024): + // Valid values (when converted to GB by multiplying the value provided by 1000): // - must be a multiple of 5GB // - > 30GB must be a multiple of 10 // - > 200GB must be a multiple of 100 diff --git a/openapi.json b/openapi.json index eb14f1e..6d13b36 100644 --- a/openapi.json +++ b/openapi.json @@ -8,7 +8,7 @@ "url": "https://support.binarylane.com.au/support/home", "email": "support@binarylane.com.au" }, - "version": "0.34.0", + "version": "0.34.4", "x-logo": { "url": "https://www.binarylane.com.au/res/images/binarylane/logo.png", "backgroundColor": "#000000", @@ -407,7 +407,8 @@ "type": "integer", "description": "The target server id.", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "responses": { @@ -1814,7 +1815,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "load-balancer list" } ], "responses": { @@ -1865,7 +1867,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "load-balancer list" } ], "requestBody": { @@ -1938,7 +1941,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "load-balancer list" } ], "responses": { @@ -2122,7 +2126,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "load-balancer list" } ], "requestBody": { @@ -2188,7 +2193,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "load-balancer list" } ], "requestBody": { @@ -2235,7 +2241,7 @@ "x-codeSamples": [ { "lang": "Curl", - "source": "curl -X DELETE \"https://api.binarylane.com.au/v2/load_balancers/${load_balancer_id}/servers\" \\\n-H \"Authorization: Bearer ${APITOKEN}\"" + "source": "curl -X DELETE \"https://api.binarylane.com.au/v2/load_balancers/${load_balancer_id}/servers\" \\\n-H \"Authorization: Bearer ${APITOKEN}\" \\\n-H \"Content-Type: application/json\" -d \"${PAYLOAD}\"" } ], "x-cli-command": "load-balancer server delete" @@ -2256,7 +2262,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "load-balancer list" } ], "requestBody": { @@ -2322,7 +2329,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "load-balancer list" } ], "requestBody": { @@ -2369,7 +2377,7 @@ "x-codeSamples": [ { "lang": "Curl", - "source": "curl -X DELETE \"https://api.binarylane.com.au/v2/load_balancers/${load_balancer_id}/forwarding_rules\" \\\n-H \"Authorization: Bearer ${APITOKEN}\"" + "source": "curl -X DELETE \"https://api.binarylane.com.au/v2/load_balancers/${load_balancer_id}/forwarding_rules\" \\\n-H \"Authorization: Bearer ${APITOKEN}\" \\\n-H \"Content-Type: application/json\" -d \"${PAYLOAD}\"" } ], "x-cli-command": "load-balancer rule delete" @@ -2527,6 +2535,9 @@ } } }, + "204": { + "description": "No Content" + }, "400": { "description": "Bad Request", "content": { @@ -2577,7 +2588,8 @@ "type": "integer", "description": "The target server id.", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "data_interval", @@ -2642,7 +2654,8 @@ "type": "integer", "description": "The target server id.", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "data_interval", @@ -2760,7 +2773,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "page", @@ -2837,7 +2851,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -3089,7 +3104,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "responses": { @@ -3139,7 +3155,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "reason", @@ -3311,7 +3328,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "action_id", @@ -3373,7 +3391,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "responses": { @@ -3425,7 +3444,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "responses": { @@ -3477,7 +3497,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "page", @@ -3553,7 +3574,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -3629,7 +3651,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "page", @@ -3708,7 +3731,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "page", @@ -3787,7 +3811,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "responses": { @@ -3879,7 +3904,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" }, { "name": "page", @@ -3957,7 +3983,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "responses": { @@ -4009,7 +4036,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "responses": { @@ -4359,7 +4387,8 @@ "type": "integer", "description": "The target vpc id.", "format": "int64" - } + }, + "x-cli-entity-lookup": "vpc list" } ], "responses": { @@ -4411,7 +4440,8 @@ "type": "integer", "description": "The target vpc id.", "format": "int64" - } + }, + "x-cli-entity-lookup": "vpc list" } ], "requestBody": { @@ -4487,7 +4517,8 @@ "type": "integer", "description": "The target vpc id.", "format": "int64" - } + }, + "x-cli-entity-lookup": "vpc list" } ], "requestBody": { @@ -4561,7 +4592,8 @@ "type": "integer", "description": "The target vpc id.", "format": "int64" - } + }, + "x-cli-entity-lookup": "vpc list" } ], "responses": { @@ -4727,7 +4759,8 @@ "type": "integer", "description": "The target vpc id.", "format": "int64" - } + }, + "x-cli-entity-lookup": "vpc list" }, { "name": "resource_type", @@ -4818,7 +4851,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -4893,7 +4927,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -4968,7 +5003,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5043,7 +5079,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5118,7 +5155,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5193,7 +5231,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5268,7 +5307,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5343,7 +5383,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5418,7 +5459,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5493,7 +5535,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5568,7 +5611,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5643,7 +5687,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5718,7 +5763,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5793,7 +5839,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5868,7 +5915,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -5943,7 +5991,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6018,7 +6067,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6093,7 +6143,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6168,7 +6219,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6243,7 +6295,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6318,7 +6371,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6393,7 +6447,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6468,7 +6523,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6543,7 +6599,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6618,7 +6675,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6693,7 +6751,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6768,7 +6827,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6843,7 +6903,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6918,7 +6979,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -6983,7 +7045,7 @@ "ServerActions" ], "summary": "Create an Additional Disk for a Server", - "description": "This is used to add an additional disk in unallocated storage space. \nThis does not alter the total disk space available to your server: to add additional disk space for your server use the 'Resize' action. \n \n", + "description": "This is used to add an additional disk in unallocated storage space. \nThis does not alter the total disk space available to your server: to add additional disk space for your server use the 'Resize' action. \n**This action may require the server to be rebooted.** \n \n", "parameters": [ { "name": "server_id", @@ -6993,7 +7055,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7068,7 +7131,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7133,7 +7197,7 @@ "ServerActions" ], "summary": "Delete an Additional Disk for a Server", - "description": "This is used to delete a disk added using the 'AddDisk' action. \n**NB: This is a destructive operation and no further confirmation will be requested.** \n \n", + "description": "This is used to delete a disk added using the 'AddDisk' action. \n**NB: This is a destructive operation and no further confirmation will be requested.** \n**This action may require the server to be rebooted.** \n \n", "parameters": [ { "name": "server_id", @@ -7143,7 +7207,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7218,7 +7283,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7293,7 +7359,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7368,7 +7435,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7443,7 +7511,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7518,7 +7587,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7593,7 +7663,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7668,7 +7739,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7743,7 +7815,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7818,7 +7891,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -7893,7 +7967,8 @@ "schema": { "type": "integer", "format": "int64" - } + }, + "x-cli-entity-lookup": "server list" } ], "requestBody": { @@ -9328,7 +9403,7 @@ }, "transfer": { "type": "number", - "description": "The total transfer per month in TB for this server.\nIf specified this is the absolute value, not just the additional transfer above what is included in the size.\nLeave null to accept the default for the size if this is a new server or a resize to a different base size, or to keep the current value if this a resize with the same base size but different options.\n \nValid values (when converted to GB by multiplying the value provided by 1024):\n- must be a multiple of 5GB\n- > 30GB must be a multiple of 10\n- > 200GB must be a multiple of 100\n- > 2000GB must be a multiple of 1000", + "description": "The total transfer per month in TB for this server.\nIf specified this is the absolute value, not just the additional transfer above what is included in the size.\nLeave null to accept the default for the size if this is a new server or a resize to a different base size, or to keep the current value if this a resize with the same base size but different options.\n \nValid values (when converted to GB by multiplying the value provided by 1000):\n- must be a multiple of 5GB\n- > 30GB must be a multiple of 10\n- > 200GB must be a multiple of 100\n- > 2000GB must be a multiple of 1000", "format": "double", "nullable": true }, @@ -11277,7 +11352,8 @@ }, "description": "The server IDs of the servers that are currently in the load balancer pool (regardless of their current 'health')." } - } + }, + "x-cli-entity-ref": "name" }, "LoadBalancerAvailabilityOption": { "required": [ @@ -11395,7 +11471,8 @@ "$ref": "#/components/schemas/LoadBalancer" } } - } + }, + "x-cli-entity-list": "load_balancers" }, "LocalNameserversResponse": { "required": [ @@ -12657,7 +12734,8 @@ ], "description": "The currently enabled advanced features, machine type and processor flags." } - } + }, + "x-cli-entity-ref": "name" }, "ServerAction": { "required": [ @@ -12922,7 +13000,8 @@ "$ref": "#/components/schemas/Server" } } - } + }, + "x-cli-entity-list": "servers" }, "Shutdown": { "required": [ @@ -13118,7 +13197,7 @@ }, "transfer_max": { "type": "number", - "description": "The maximum transfer in TB permitted for this size.", + "description": "The maximum transfer in TB permitted for this size. If this is the same as Size.Transfer no additional transfer is supported.", "format": "double" }, "transfer_cost_per_additional_gigabyte": { @@ -13231,7 +13310,7 @@ }, "transfer": { "type": "number", - "description": "The total transfer per month in TB for this server.\nIf specified this is the absolute value, not just the additional transfer above what is included in the size.\nLeave null to accept the default for the size if this is a new server or a resize to a different base size, or to keep the current value if this a resize with the same base size but different options.\n \nValid values (when converted to GB by multiplying the value provided by 1024):\n- must be a multiple of 5GB\n- > 30GB must be a multiple of 10\n- > 200GB must be a multiple of 100\n- > 2000GB must be a multiple of 1000", + "description": "The total transfer per month in TB for this server.\nIf specified this is the absolute value, not just the additional transfer above what is included in the size.\nLeave null to accept the default for the size if this is a new server or a resize to a different base size, or to keep the current value if this a resize with the same base size but different options.\n \nValid values (when converted to GB by multiplying the value provided by 1000):\n- must be a multiple of 5GB\n- > 30GB must be a multiple of 10\n- > 200GB must be a multiple of 100\n- > 2000GB must be a multiple of 1000", "format": "double", "nullable": true } @@ -14148,7 +14227,8 @@ }, "description": "The route entries that control how network traffic is directed through the VPC environment." } - } + }, + "x-cli-entity-ref": "name" }, "VpcMember": { "required": [ @@ -14256,7 +14336,8 @@ "$ref": "#/components/schemas/Vpc" } } - } + }, + "x-cli-entity-list": "vpcs" } }, "securitySchemes": { diff --git a/scripts/deploy-cluster.sh b/scripts/deploy-cluster.sh index de16a00..0bab189 100755 --- a/scripts/deploy-cluster.sh +++ b/scripts/deploy-cluster.sh @@ -233,20 +233,101 @@ create_server() { # Generate random password to avoid email notifications local random_password=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32) - local data=$(cat < /dev/null + +curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.35/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg +echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.35/deb/ /" | tee /etc/apt/sources.list.d/kubernetes.list +KUBE_VERSION="1.35.0-1.1" + +apt-get update + +# Install required packages +apt-get install -y --no-install-recommends \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg \ + containerd.io \ + kubelet=$KUBE_VERSION kubeadm=$KUBE_VERSION kubectl=$KUBE_VERSION + +apt-mark hold kubelet kubeadm kubectl + +# Configure containerd +mkdir -p /etc/containerd +containerd config default | tee /etc/containerd/config.toml > /dev/null +systemctl restart containerd + +# Disable swap +swapoff -a +sed -i '/ swap / s/^/#/' /etc/fstab + +# Load kernel modules +cat > /etc/modules-load.d/k8s.conf <<'EOL' +overlay +br_netfilter +EOL +modprobe overlay +modprobe br_netfilter + +# Set kernel parameters +cat > /etc/sysctl.d/k8s.conf <<'EOL' +net.bridge.bridge-nf-call-iptables = 1 +net.bridge.bridge-nf-call-ip6tables = 1 +net.ipv4.ip_forward = 1 +EOL +sysctl --system + +# Enable kubelet service +systemctl enable kubelet + +# Configure kubelet to use external cloud provider +mkdir -p /etc/systemd/system/kubelet.service.d +cat > /etc/systemd/system/kubelet.service.d/20-cloud-provider.conf <<'EOL' +[Service] +Environment="KUBELET_EXTRA_ARGS=--cloud-provider=external" +EOL +systemctl daemon-reload + +# Pre-pull Kubernetes images +kubeadm config images pull + +echo "Kubernetes installation complete" +USERDATA ) + # Escape user_data for JSON (escape backslashes, quotes, and convert newlines) + local user_data_json=$(echo "$user_data_script" | jq -Rs .) + + # Create JSON payload + local data=$(jq -n \ + --arg name "$name" \ + --arg region "$REGION" \ + --arg size "$SERVER_SIZE" \ + --argjson image "$image_id" \ + --argjson ssh_key "$SSH_KEY_ID" \ + --arg password "$random_password" \ + --argjson vpc "$VPC_ID" \ + --argjson user_data "$user_data_json" \ + '{ + name: $name, + region: $region, + size: $size, + image: $image, + ssh_keys: [$ssh_key], + password: $password, + backups: false, + vpc_id: $vpc, + user_data: $user_data + }' + ) + log_info "Creating with: region=$REGION, size=$SERVER_SIZE, image=$image_id, ssh_key=$SSH_KEY_ID, vpc_id=$VPC_ID" local response=$(api_call POST "/servers" "$data") @@ -281,6 +362,35 @@ EOF fi log_success "Created server: $name (ID: $server_id, IP: $server_ip)" + + # Remove old SSH host key (BinaryLane recycles IPs) + ssh-keygen -f "$HOME/.ssh/known_hosts" -R "$server_ip" >/dev/null 2>&1 || true + + # Wait for SSH to be available + log_info "Waiting for SSH on $name ($server_ip)..." + local ssh_attempts=0 + while [ $ssh_attempts -lt 60 ]; do + if ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=3 -o BatchMode=yes root@$server_ip "exit 0" >/dev/null 2>&1; then + log_success "SSH ready on $name" + break + fi + echo -n "." >&2 + sleep 2 + ssh_attempts=$((ssh_attempts + 1)) + done + echo "" >&2 + + # Wait for cloud-init to complete + log_info "Waiting for cloud-init to complete on $name..." + ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ServerAliveInterval=60 -o ServerAliveCountMax=5 root@$server_ip bash >&2 <<'EOSSH' +set -e +# tail is noisy, uncomment for debugging +# tail -f /var/log/cloud-init-output.log 2>/dev/null || echo "Cloud-init log not yet available" +cloud-init status --wait || cloud-init status --format json +echo "Cloud-init completed successfully" +EOSSH + log_success "Cloud-init complete on $name" + echo "$server_id:$server_ip" } @@ -365,129 +475,24 @@ get_or_create_servers() { log_success "All servers ready" } -wait_for_ssh() { - local ip="$1" - local hostname="${2:-server}" - local max_attempts=60 - local attempt=0 - - log_info "Waiting for SSH on $hostname ($ip)..." - - while [ $attempt -lt $max_attempts ]; do - if ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=3 -o BatchMode=yes root@$ip "echo 'SSH ready'" 2>/dev/null; then - echo "" >&2 # New line after dots - log_success "SSH ready on $hostname ($ip)" - return 0 - fi - - # Show progress every 12 attempts - if [ $((attempt % 12)) -eq 0 ] && [ $attempt -gt 0 ]; then - log_info "Still waiting for SSH (attempt $attempt/$max_attempts)..." >&2 - fi - - echo -n "." >&2 - sleep 2 - attempt=$((attempt + 1)) - done - - echo "" >&2 # New line after dots - log_error "SSH did not become ready on $hostname ($ip) after $((max_attempts * 2)) seconds" - log_error "Please verify:" - log_error " 1. SSH key is added to your BinaryLane account" - log_error " 2. Server can be accessed at: ssh root@$ip" - log_error " 3. Security groups allow SSH access" - return 1 -} - -install_kubernetes_prerequisites() { - local ip="$1" - local hostname="${2:-server}" - - log_info "Installing Kubernetes prerequisites on $hostname ($ip)..." +initialize_control_plane() { + log_info "Initializing Kubernetes control plane..." - # Check if already installed - if ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=5 -o BatchMode=yes root@$ip "which kubeadm" &>/dev/null; then - log_info "Kubernetes already installed on $hostname" - return 0 + # Validate CONTROL_PLANE_IP is set + if [ -z "${CONTROL_PLANE_IP:-}" ]; then + log_error "CONTROL_PLANE_IP is not set. Cannot initialize control plane." + return 1 fi - ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no root@$ip bash <<'EOF' -set -euo pipefail - -# Update system - -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg -echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null - -curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg -echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /" | tee /etc/apt/sources.list.d/kubernetes.list -KUBE_VERSION="1.33.3-1.1" - -apt-get update - -# Install required packages -apt-get install -y --no-install-recommends \ - apt-transport-https \ - ca-certificates \ - curl \ - gnupg \ - containerd.io \ - kubelet=$KUBE_VERSION kubeadm=$KUBE_VERSION kubectl=$KUBE_VERSION - -apt-mark hold kubelet kubeadm kubectl - -# Configure containerd -mkdir -p /etc/containerd -containerd config default | tee /etc/containerd/config.toml > /dev/null -systemctl restart containerd - -# Disable swap -swapoff -a -sed -i '/ swap / s/^/#/' /etc/fstab - -# Load kernel modules -cat > /etc/modules-load.d/k8s.conf <<'EOL' -overlay -br_netfilter -EOL -modprobe overlay -modprobe br_netfilter - -# Set kernel parameters -cat > /etc/sysctl.d/k8s.conf <<'EOL' -net.bridge.bridge-nf-call-iptables = 1 -net.bridge.bridge-nf-call-ip6tables = 1 -net.ipv4.ip_forward = 1 -EOL -sysctl --system - -# Enable kubelet service -systemctl enable kubelet - -# Configure kubelet to use external cloud provider -mkdir -p /etc/systemd/system/kubelet.service.d -cat > /etc/systemd/system/kubelet.service.d/20-cloud-provider.conf <<'EOL' -[Service] -Environment="KUBELET_EXTRA_ARGS=--cloud-provider=external" -EOL -systemctl daemon-reload - -echo "Kubernetes installation complete" -EOF - - log_success "Kubernetes installed on $hostname" -} - -initialize_control_plane() { - log_info "Initializing Kubernetes control plane..." + log_info "Control plane IP: $CONTROL_PLANE_IP" # Check if cluster is already initialized - if ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no root@$CONTROL_PLANE_IP "test -f /etc/kubernetes/admin.conf" 2>/dev/null; then + if ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no root@"$CONTROL_PLANE_IP" "test -f /etc/kubernetes/admin.conf" 2>/dev/null; then log_info "Control plane already initialized" return 0 fi - ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no root@$CONTROL_PLANE_IP bash </dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'") - local token=$(ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no root@$CONTROL_PLANE_IP \ + local token=$(ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ServerAliveInterval=60 root@$CONTROL_PLANE_IP \ "kubeadm token create") - ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no root@$worker_ip bash <