feat: initial

This commit is contained in:
Maximilian Jugl 2026-05-05 14:16:44 +02:00
commit 638004c7ee
8 changed files with 444 additions and 0 deletions

View file

@ -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 }}

203
.gitignore vendored Normal file
View file

@ -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

3
.prettierrc Normal file
View file

@ -0,0 +1,3 @@
{
"printWidth": 120
}

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2
}

47
README.md Normal file
View file

@ -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`.

130
action.yml Normal file
View file

@ -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

28
package-lock.json generated Normal file
View file

@ -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"
}
}
}
}

5
package.json Normal file
View file

@ -0,0 +1,5 @@
{
"devDependencies": {
"prettier": "^3.8.3"
}
}