commit 638004c7ee73e83ad8f03aee2344e17ba64f7ad2 Author: Maximilian Jugl Date: Tue May 5 14:16:44 2026 +0200 feat: initial diff --git a/.forgejo/workflows/check-pr-title.yaml b/.forgejo/workflows/check-pr-title.yaml new file mode 100644 index 0000000..3a615a8 --- /dev/null +++ b/.forgejo/workflows/check-pr-title.yaml @@ -0,0 +1,23 @@ +name: Check PR title + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - edited + +jobs: + check: + runs-on: stackit-docker + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + sparse-checkout: | + .forgejo + - name: Check PR title + uses: https://stackit-solutions.git.onstackit.cloud/actions/check-conventional-commit@v1 + with: + value: ${{ github.event.pull_request.title }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1a25024 --- /dev/null +++ b/.gitignore @@ -0,0 +1,203 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.* +!.env.example + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist +.output + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp directory +.temp + +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# Firebase cache directory +.firebase/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# pnpm +.pnpm-store + +# yarn v3 +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ + +# General +.DS_Store +.localized +__MACOSX/ +.AppleDouble +.LSOverride +Icon[ +] + +# Resource forks +._* + +# Files and directories that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.com.apple.timemachine.supported +.PKInstallSandboxManager +.PKInstallSandboxManager-SystemSoftware +.hotfiles.btree +.vol +.file +.disk_label* +lost+found +.HFS+ Private Directory Data[ +] + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Mac OS 6 to 9 +Desktop DB +Desktop DF +TheFindByContentFolder +TheVolumeSettingsFolder +.FBCIndex +.FBCSemaphoreFile +.FBCLockFolder + +# Quota system +.quota.group +.quota.user +.quota.ops.group +.quota.ops.user + +# TimeMachine +Backups.backupdb +.MobileBackups +.MobileBackups.trash +MobileBackups.trash +tmbootpicker.efi \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..94d737c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d9b59f7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcbd1e1 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# SKE Operations Action + +This action calls SKE cluster operations from your pipelines. + +## Usage + +To use this action in your workflow, reference it as a step. + +The action assumes that the STACKIT CLI and jq are installed. +If you're using the [install-stackit-cli action](https://stackit-solutions.git.onstackit.cloud/actions/install-stackit-cli), then these applications will already be installed for you. + +You also require a STACKIT service account key. +The key must be tied to a service account which has editor privileges on SKE resources. +In the following example, the service account key is provided to the runner as a base64-encoded secret. + +```yaml +- name: Install STACKIT CLI + uses: https://stackit-solutions.git.onstackit.cloud/actions/install-stackit-cli@v1 +- name: Set up STACKIT service account + env: + SA_KEY_B64: ${{ secrets.STACKIT_SERVICE_ACCOUNT_KEY_B64 }} + run: | + set -eo pipefail + + TMP_SA_KEY_PATH="$(mktemp)" + echo "Writing STACKIT service account key to $TMP_SA_KEY_PATH" + + echo "$SA_KEY_B64" | base64 -d > "$TMP_SA_KEY_PATH" + echo "STACKIT_SERVICE_ACCOUNT_KEY_PATH=$TMP_SA_KEY_PATH" >> "$GITHUB_ENV" +- name: Hibernate cluster + uses: https://stackit-solutions.git.onstackit.cloud/actions/ske-operations@v1 + with: + action: hibernate + region: eu01 + project-id: 01234567-89ab-cdef-0123-456789abcdef + cluster-name: foo-cluster +``` + +## Inputs + +- `action` (Required): Operation to perform. Must be one of: `wake`, `hibernate`, `reconcile`, `maintenance`. +- `region` (Required): Data center region of SKE cluster. +- `project-id` (Required): STACKIT project ID. +- `cluster-name` (Required): Name of SKE cluster. +- `timeout-seconds` (Optional): Time in seconds after which target state check fails. Default is `900`. +- `interval-seconds` (Optional): Time in seconds between target state checks. Default is `5`. +- `wait` (Optional): Wait until target state of operation has been reached. Default is `true`. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..2e6fe53 --- /dev/null +++ b/action.yml @@ -0,0 +1,130 @@ +name: "Trigger SKE operations" +description: "Performs operations on SKE clusters (wake, hibernate, reconcile, maintenance)." +inputs: + action: + description: "Operation to perform." + required: true + region: + description: "Data center region of SKE cluster." + required: true + project-id: + description: "STACKIT project ID." + required: true + cluster-name: + description: "Name of SKE cluster." + required: true + timeout-seconds: + description: "Time in seconds after which target state check fails." + required: false + default: "900" + interval-seconds: + description: "Time in seconds between target state checks." + required: false + default: "5" + wait: + description: "Wait until target state of operation has been reached." + required: false + default: "true" + +runs: + using: composite + steps: + - name: Trigger SKE operation + shell: sh + env: + INPUT_ACTION: ${{ inputs.action }} + INPUT_REGION: ${{ inputs.region }} + INPUT_PROJECT_ID: ${{ inputs.project-id }} + INPUT_CLUSTER_NAME: ${{ inputs.cluster-name }} + INPUT_TIMEOUT_SECS: ${{ inputs.timeout-seconds }} + INPUT_INTERVAL_SECS: ${{ inputs.interval-seconds }} + INPUT_WAIT: ${{ inputs.wait }} + run: | + set -e + + for cmd in stackit jq grep; do + if ! command -v "$cmd" > /dev/null 2>&1; then + echo "'$cmd' not installed." + exit 1 + fi + done + + URL_SAFE_REGEX="^[a-zA-Z0-9._~-]+$" + + for input in "$INPUT_ACTION" "$INPUT_REGION" "$INPUT_PROJECT_ID" "$INPUT_CLUSTER_NAME"; do + if ! printf "%s\n" "$input" | grep -Eq "$URL_SAFE_REGEX"; then + echo "'$input' is not a URL-safe input." + exit 1 + fi + done + + TIMEOUT_SECS="${INPUT_TIMEOUT_SECS:-900}" + INTERVAL_SECS="${INPUT_INTERVAL_SECS:-5}" + WAIT="${INPUT_WAIT:-true}" + + case "$INPUT_ACTION" in + hibernate) + TARGET_STATUS="STATE_HIBERNATED" + INITIAL_STATUS="STATE_HEALTHY" + ENDPOINT="hibernate" + ;; + wake) + TARGET_STATUS="STATE_HEALTHY" + INITIAL_STATUS="STATE_HIBERNATED" + ENDPOINT="wakeup" + ;; + reconcile) + TARGET_STATUS="STATE_HEALTHY" + INITIAL_STATUS="STATE_HEALTHY" + ENDPOINT="reconcile" + ;; + maintenance) + TARGET_STATUS="STATE_HEALTHY" + INITIAL_STATUS="STATE_HEALTHY" + ENDPOINT="maintenance" + ;; + *) + echo "Unexpected action '$INPUT_ACTION'. Must be one of: wake, hibernate, reconcile, maintenance." + exit 1 + ;; + esac + + CURRENT_STATUS="$(stackit ske cluster describe "$INPUT_CLUSTER_NAME" -ojson | jq -r '.status.aggregated')" + + if [ "$CURRENT_STATUS" != "$INITIAL_STATUS" ]; then + if [ "$CURRENT_STATUS" = "$TARGET_STATUS" ]; then + echo "Cluster is already in desired state." + exit 0 + fi + + echo "Unexpected cluster status: $CURRENT_STATUS." + exit 1 + fi + + stackit curl --fail -X POST "https://ske.api.stackit.cloud/v2/projects/$INPUT_PROJECT_ID/regions/$INPUT_REGION/clusters/$INPUT_CLUSTER_NAME/$ENDPOINT" + + if [ "$WAIT" != "true" ]; then + echo "Operation triggered. No wait requested, exiting." + exit 0 + fi + + START_TIME="$(date +%s)" + SECS_ELAPSED=0 + + while [ "$SECS_ELAPSED" -lt "$TIMEOUT_SECS" ]; do + CURRENT_STATUS="$(stackit ske cluster describe "$INPUT_CLUSTER_NAME" -ojson | jq -r '.status.aggregated')" + + if [ "$CURRENT_STATUS" = "$TARGET_STATUS" ]; then + echo "Targeted state reached after $SECS_ELAPSED seconds, exiting." + exit 0 + fi + + echo "Target state not reached, waiting ${INTERVAL_SECS}s... (elapsed: ${SECS_ELAPSED}s)" + sleep "$INTERVAL_SECS" + + CURRENT_TIME="$(date +%s)" + SECS_ELAPSED=$((CURRENT_TIME - START_TIME)) + done + + echo "::error::Target state not reached after $SECS_ELAPSED seconds, exiting." + exit 1 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..889127b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "ske-operations", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "prettier": "^3.8.3" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c689305 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "prettier": "^3.8.3" + } +}