Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: actionlint

on:
pull_request:
branches: [prod-staging]
paths:
- '.github/workflows/**'

# Automatically cancel in-progress actions on the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}
cancel-in-progress: true

jobs:
actionlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install action linter
run: |
mkdir -p "$HOME"/.local/bin
curl -sL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash -s -- latest "$HOME"/.local/bin

- name: Check that all workflows are valid
run: actionlint -verbose
Comment on lines +16 to +26

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}

Copilot Autofix

AI 6 months ago

The best way to fix this problem is to add an explicit permissions block that restricts the privileges granted to the GITHUB_TOKEN used within this workflow. Since the workflow only runs actionlint (a linter) on workflows and does not require write access to contents, issues, or pull requests, we should add permissions: contents: read at the job level (actionlint under jobs). This gives minimal necessary access, helping to enforce POLP.

To implement, add the following keys under the actionlint job (e.g., just above runs-on:). No new methods, definitions, or imports are needed; just YAML key insertion. There is no indication in the shown snippet that any additional permissions are required, so only contents: read should be specified.

Suggested changeset 1
.github/workflows/actionlint.yaml

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/.github/workflows/actionlint.yaml b/.github/workflows/actionlint.yaml
--- a/.github/workflows/actionlint.yaml
+++ b/.github/workflows/actionlint.yaml
@@ -13,6 +13,8 @@
 
 jobs:
   actionlint:
+    permissions:
+      contents: read
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
EOF
@@ -13,6 +13,8 @@

jobs:
actionlint:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Copilot is powered by AI and may make mistakes. Always verify output.
81 changes: 81 additions & 0 deletions .github/workflows/check-pr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: check/pr

on:
pull_request:
types: [ opened, synchronize, reopened ]
branches: [ prod-staging ]

permissions:
contents: read

jobs:
commits:
runs-on: ubuntu-latest
steps:
- name: format
if: always()
uses: taskmedia/action-conventional-commits@v1.1.20
with:
types: "build|ci|docs|feat|fix|perf|refactor|style|test|revert|chore|sync"
token: ${{ secrets.GH_PAT }}

- name: length
if: ${{ github.actor != 'dependabot' && github.actor != 'dependabot[bot]' }}
uses: gsactions/commit-message-checker@v2
with:
pattern: '((^(?=(?:.|\n)*(?:^|\n)\[\d\]: .{69,}(?:$|\n)(?:.|\n)*)(?:.|\n)*$)|(^(?!(?:.|\n)*(?:^|\n).{74,}(?:$|\n)(?:.|\n)*)(?:.|\n)*$))'
flags: ''
error: 'The maximum line length of 74 characters is exceeded.'
excludeDescription: 'true'
excludeTitle: 'true'
checkAllCommitMessages: 'true'
accessToken: ${{ secrets.GH_PAT }}

- name: signed-off-by
if: always()
uses: gsactions/commit-message-checker@v2
with:
pattern: '^Signed-off-by: .+ \<.+\@.+\..+\>$'
error: 'Signed-off-by line is missing.'
excludeDescription: 'true'
excludeTitle: 'true'
checkAllCommitMessages: 'true'
accessToken: ${{ secrets.GH_PAT }}

pr:
runs-on: ubuntu-latest
steps:
- name: title-format
if: always()
uses: gsactions/commit-message-checker@v2
with:
pattern: '^(build|ci|docs|feat|fix|perf|refactor|style|test|revert|chore|sync)(\([\w\-\_\d]+\))?!?: '
error: 'The PR title must follow the conventional commits format.'
excludeDescription: 'true'
excludeTitle: 'false'
checkAllCommitMessages: 'false'
accessToken: ${{ secrets.GH_PAT }}

- name: title-length
if: ${{ github.actor != 'dependabot' && github.actor != 'dependabot[bot]' }}
uses: gsactions/commit-message-checker@v2
with:
pattern: '^(?!.{75,}).*'
flags: ''
error: 'The maximum line length of 75 characters is exceeded.'
excludeDescription: 'true'
excludeTitle: 'false'
checkAllCommitMessages: 'false'
accessToken: ${{ secrets.GH_PAT }}

- name: description
if: ${{ github.actor != 'dependabot' && github.actor != 'dependabot[bot]' }}
uses: gsactions/commit-message-checker@v2
with:
pattern: '^\S+( \S+)*$'
error: 'The PR description must not be empty.'
flags: 'gm'
excludeDescription: 'false'
excludeTitle: 'true'
checkAllCommitMessages: 'false'
accessToken: ${{ secrets.GH_PAT }}
31 changes: 31 additions & 0 deletions .github/workflows/release-stable.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: release-stable

on:
push:
branches: [prod-staging]

permissions:
deployments: write
contents: read

jobs:
release:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
ref: prod-stable

- name: Reset promotion branch
run: |
git fetch origin prod-staging:prod-staging
git reset --hard prod-staging

- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GH_PAT }}
base: prod-stable
title: "release: Merge latest prod-staging changes to prod-stable"
branch: prod-staging
60 changes: 60 additions & 0 deletions .github/workflows/sync.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: sync

on:
workflow_dispatch:
inputs:
channel:
default: prod-stable
description: |
The name of the release channel.
One of: [prod-stable, prod-staging]

# Automatically cancel in-progress actions on the same branch
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }}
cancel-in-progress: true

permissions:
contents: write

jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.channel }}

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Generate
shell: bash
env:
CHANNEL: ${{ inputs.channel }}
run: |
make generate

- name: Show diff
shell: bash
run: |
git diff

- name: Generate branch ID
id: id
run: |
BRANCH="sync/$(date +'%Y%m%d%H%M%S')"
echo "name=${BRANCH}" >>"${GITHUB_OUTPUT}"

- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GH_PAT }}
base: ${{ inputs.channel }}
commit-message: "sync: Update Python SDK from OpenAPI spec"
title: "sync: Update Python SDK from OpenAPI spec"
branch: ${{ steps.id.outputs.name }}
committer: Unikraft Bot <monkey@unikraft.io>
author: Unikraft Bot <monkey@unikraft.io>
add-paths: controlplane,platform
signoff: true
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
__pycache__/
build/
dist/
*.egg-info/
.pytest_cache/

# pyenv
.python-version

# Environments
.env
.venv

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# JetBrains
.idea/

/coverage.xml
/.coverage
4 changes: 4 additions & 0 deletions .platform-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
project_name_override: unikraft-cloud-platform
package_name_override: unikraft_cloud_platform
literal_enums: true
docstrings_on_attributes: true
28 changes: 28 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
BSD 3-Clause License

Copyright (c) 2025, Unikraft GmbH. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025, Unikraft GmbH.
# Licensed under the BSD-3-Clause License (the "License").
# You may not use this file except in compliance with the License.

# Prelude
WORKDIR ?= $(CURDIR)
Q ?= @
CHANNEL ?= prod-stable

# Tools
WGET ?= wget
UV ?= uv

.PHONY: all
all: generate

.PHONY: generate
generate: platform

.PHONY: platform
platform:
$(Q)rm -rf $(WORKDIR)/unikraft_cloud_platform
$(Q)$(UV) tool run openapi-python-client generate \
--url https://raw.githubusercontent.com/unikraft-cloud/openapi/$(CHANNEL)/platform.json \
--config $(WORKDIR)/.platform-config.yaml \
--custom-template-path $(WORKDIR)/templates \
--overwrite \
--output-path $(WORKDIR) \
--meta uv \
95 changes: 95 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Unikraft Cloud Python SDK

This repository contains an auto-generated Go SDK which interfaces with
[Unikraft Cloud](https://unikraft.cloud) based on the public
[OpenAPI](https://github.com/unikraft-cloud/openapi) specification.

> **Get started with Unikraft Cloud Today**
>
> Sign up at https://console.unikraft.cloud/signup

## Quickstart

```python
#!/usr/bin/env python3
"""
List Instances Example

This example demonstrates how to use the Unikraft Cloud Platform SDK to list instances.

Usage:
python list_instances.py

Environment Variables:
UKC_TOKEN: Your Unikraft Cloud API token (required)
UKC_METRO: The metro URL (optional, defaults to fra0.kraft.cloud)

Example:
export UKC_TOKEN="your-api-token-here"
export UKC_METRO="https://api.fra0.kraft.cloud"
python list_instances.py
"""

import os
import sys
from unikraft_cloud_platform import AuthenticatedClient
from unikraft_cloud_platform.models import Instance
from unikraft_cloud_platform.models.get_instances_response import GetInstancesResponse
from unikraft_cloud_platform.api.instances import get_instances
from unikraft_cloud_platform.types import Response


def main():
# Read configuration from environment variables
token = os.getenv("UKC_TOKEN")
base_url = os.getenv("UKC_METRO", "https://api.fra0.kraft.cloud")

if not token:
print("Error: UKC_TOKEN environment variable is required", file=sys.stderr)
print("Please set your API token: export UKC_TOKEN='your-token-here'", file=sys.stderr)
sys.exit(1)

client = AuthenticatedClient(
base_url=base_url,
token=token,
)

# List all instances (empty body means get all instances)
with client as client:
response: Response[GetInstancesResponse] = get_instances.sync_detailed(
client=client,
body=[], # Empty list to get all instances
details=True
)

# Check if the request was successful
if response.status_code == 200 and response.parsed:
instances_response = response.parsed

# Check if we have data and instances
if instances_response.data and instances_response.data.instances:
print(f"Found {len(instances_response.data.instances)} instances:")
print("-" * 50)

for instance in instances_response.data.instances:
print(f"Name: {instance.name}")
print(f"UUID: {instance.uuid}")
print(f"State: {instance.state}")
print(f"Created: {instance.created_at}")
if instance.private_fqdn:
print(f"Private FQDN: {instance.private_fqdn}")
print("-" * 50)
else:
print("No instances found.")
else:
print(f"Failed to retrieve instances. Status code: {response.status_code}")
if response.parsed and response.parsed.message:
print(f"Error message: {response.parsed.message}")
if response.parsed and response.parsed.errors:
for error in response.parsed.errors:
print(f"Error: {error}")


if __name__ == "__main__":
main()
```
Loading
Loading