exoscale-connector 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exoscale_connector-0.2.0/.github/workflows/ci.yml +29 -0
- exoscale_connector-0.2.0/.github/workflows/release.yml +51 -0
- exoscale_connector-0.2.0/.gitignore +81 -0
- exoscale_connector-0.2.0/LICENSE +21 -0
- exoscale_connector-0.2.0/PKG-INFO +150 -0
- exoscale_connector-0.2.0/README.md +125 -0
- exoscale_connector-0.2.0/docs/asset-types/README.md +72 -0
- exoscale_connector-0.2.0/docs/asset-types/anti-affinity-group.md +65 -0
- exoscale_connector-0.2.0/docs/asset-types/api-key.md +88 -0
- exoscale_connector-0.2.0/docs/asset-types/block-volume-snapshot.md +96 -0
- exoscale_connector-0.2.0/docs/asset-types/block-volume.md +118 -0
- exoscale_connector-0.2.0/docs/asset-types/dbaas.md +160 -0
- exoscale_connector-0.2.0/docs/asset-types/dns.md +112 -0
- exoscale_connector-0.2.0/docs/asset-types/elastic-ip.md +100 -0
- exoscale_connector-0.2.0/docs/asset-types/iam-role.md +149 -0
- exoscale_connector-0.2.0/docs/asset-types/iam-user.md +62 -0
- exoscale_connector-0.2.0/docs/asset-types/instance-pool.md +108 -0
- exoscale_connector-0.2.0/docs/asset-types/instance-type.md +43 -0
- exoscale_connector-0.2.0/docs/asset-types/instance.md +136 -0
- exoscale_connector-0.2.0/docs/asset-types/load-balancer.md +152 -0
- exoscale_connector-0.2.0/docs/asset-types/object-storage.md +137 -0
- exoscale_connector-0.2.0/docs/asset-types/private-network.md +83 -0
- exoscale_connector-0.2.0/docs/asset-types/security-group.md +119 -0
- exoscale_connector-0.2.0/docs/asset-types/sks.md +184 -0
- exoscale_connector-0.2.0/docs/asset-types/snapshot.md +102 -0
- exoscale_connector-0.2.0/docs/asset-types/ssh-key.md +83 -0
- exoscale_connector-0.2.0/docs/asset-types/template.md +48 -0
- exoscale_connector-0.2.0/docs/asset-types/zone.md +37 -0
- exoscale_connector-0.2.0/docs/developer-guide.md +217 -0
- exoscale_connector-0.2.0/docs/iam-policy-cookbook.md +198 -0
- exoscale_connector-0.2.0/docs/live-test-plan.md +389 -0
- exoscale_connector-0.2.0/docs/live-test-results.md +329 -0
- exoscale_connector-0.2.0/docs/roadmap.md +116 -0
- exoscale_connector-0.2.0/docs/user-guide.md +184 -0
- exoscale_connector-0.2.0/pyproject.toml +90 -0
- exoscale_connector-0.2.0/src/exoscale_connector/__init__.py +48 -0
- exoscale_connector-0.2.0/src/exoscale_connector/auth.py +78 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/__init__.py +7 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/_base.py +338 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/anti_affinity_group.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/api_key.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/block_volume.py +27 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/block_volume_snapshot.py +33 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/dbaas.py +97 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/dns.py +42 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/elastic_ip.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/iam_role.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/iam_user.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/instance.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/instance_pool.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/instance_type.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/load_balancer.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/main.py +85 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/object_storage.py +160 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/private_network.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/security_group.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/sks.py +43 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/snapshot.py +34 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/ssh_key.py +25 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/template.py +31 -0
- exoscale_connector-0.2.0/src/exoscale_connector/cli/zone.py +23 -0
- exoscale_connector-0.2.0/src/exoscale_connector/client.py +262 -0
- exoscale_connector-0.2.0/src/exoscale_connector/config.py +110 -0
- exoscale_connector-0.2.0/src/exoscale_connector/errors.py +76 -0
- exoscale_connector-0.2.0/src/exoscale_connector/iam_expr.py +80 -0
- exoscale_connector-0.2.0/src/exoscale_connector/models.py +79 -0
- exoscale_connector-0.2.0/src/exoscale_connector/py.typed +0 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/__init__.py +8 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/_base.py +255 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/_reverse_dns.py +85 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/anti_affinity_group.py +39 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/api_key.py +59 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/block_volume.py +159 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/block_volume_snapshot.py +95 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/dbaas.py +383 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/dns.py +206 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/elastic_ip.py +50 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/iam_role.py +237 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/iam_user.py +44 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/instance.py +146 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/instance_pool.py +68 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/instance_type.py +49 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/load_balancer.py +144 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/object_storage.py +390 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/private_network.py +31 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/security_group.py +93 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/sks.py +237 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/snapshot.py +94 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/ssh_key.py +44 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/template.py +82 -0
- exoscale_connector-0.2.0/src/exoscale_connector/resources/zone.py +39 -0
- exoscale_connector-0.2.0/src/exoscale_connector/wait.py +53 -0
- exoscale_connector-0.2.0/tests/conftest.py +36 -0
- exoscale_connector-0.2.0/tests/integration/__init__.py +1 -0
- exoscale_connector-0.2.0/tests/integration/_fixtures.py +165 -0
- exoscale_connector-0.2.0/tests/integration/_recorder.py +85 -0
- exoscale_connector-0.2.0/tests/integration/conftest.py +190 -0
- exoscale_connector-0.2.0/tests/integration/test_smoke.py +106 -0
- exoscale_connector-0.2.0/tests/integration/test_tier_1.py +290 -0
- exoscale_connector-0.2.0/tests/integration/test_tier_2.py +168 -0
- exoscale_connector-0.2.0/tests/integration/test_tier_3.py +329 -0
- exoscale_connector-0.2.0/tests/integration/test_tier_4.py +305 -0
- exoscale_connector-0.2.0/tests/recorded/README.md +17 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t210654z-hm5r27.jsonl +71 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t211130z-qint3r.jsonl +39 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t212438z-zc5fk2.jsonl +22 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t212710z-vha6f5.jsonl +149 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t213124z-eumxat.jsonl +83 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t213339z-bxntit.jsonl +83 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t213541z-6912dm.jsonl +64 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t213656z-ufn85e.jsonl +334 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t214915z-ohlx0g.jsonl +43 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t215040z-3424x5.jsonl +92 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t215314z-myjv55.jsonl +40 -0
- exoscale_connector-0.2.0/tests/recorded/live-20260610t215606z-hf3cnq.jsonl +136 -0
- exoscale_connector-0.2.0/tests/unit/test_anti_affinity_group.py +103 -0
- exoscale_connector-0.2.0/tests/unit/test_api_key.py +132 -0
- exoscale_connector-0.2.0/tests/unit/test_auth.py +39 -0
- exoscale_connector-0.2.0/tests/unit/test_block_volume.py +193 -0
- exoscale_connector-0.2.0/tests/unit/test_block_volume_snapshot.py +152 -0
- exoscale_connector-0.2.0/tests/unit/test_cli.py +192 -0
- exoscale_connector-0.2.0/tests/unit/test_cli_main.py +83 -0
- exoscale_connector-0.2.0/tests/unit/test_client.py +214 -0
- exoscale_connector-0.2.0/tests/unit/test_config.py +34 -0
- exoscale_connector-0.2.0/tests/unit/test_dbaas.py +333 -0
- exoscale_connector-0.2.0/tests/unit/test_dns.py +161 -0
- exoscale_connector-0.2.0/tests/unit/test_elastic_ip.py +131 -0
- exoscale_connector-0.2.0/tests/unit/test_ensure.py +89 -0
- exoscale_connector-0.2.0/tests/unit/test_iam_expr.py +54 -0
- exoscale_connector-0.2.0/tests/unit/test_iam_role.py +332 -0
- exoscale_connector-0.2.0/tests/unit/test_iam_user.py +119 -0
- exoscale_connector-0.2.0/tests/unit/test_instance.py +142 -0
- exoscale_connector-0.2.0/tests/unit/test_instance_pool.py +127 -0
- exoscale_connector-0.2.0/tests/unit/test_instance_type.py +32 -0
- exoscale_connector-0.2.0/tests/unit/test_load_balancer.py +173 -0
- exoscale_connector-0.2.0/tests/unit/test_models.py +42 -0
- exoscale_connector-0.2.0/tests/unit/test_object_storage.py +353 -0
- exoscale_connector-0.2.0/tests/unit/test_private_network.py +131 -0
- exoscale_connector-0.2.0/tests/unit/test_recorded_replay.py +58 -0
- exoscale_connector-0.2.0/tests/unit/test_recorder.py +67 -0
- exoscale_connector-0.2.0/tests/unit/test_reverse_dns.py +95 -0
- exoscale_connector-0.2.0/tests/unit/test_security_group.py +86 -0
- exoscale_connector-0.2.0/tests/unit/test_sks.py +181 -0
- exoscale_connector-0.2.0/tests/unit/test_snapshot.py +152 -0
- exoscale_connector-0.2.0/tests/unit/test_ssh_key.py +137 -0
- exoscale_connector-0.2.0/tests/unit/test_template.py +49 -0
- exoscale_connector-0.2.0/tests/unit/test_wait.py +43 -0
- exoscale_connector-0.2.0/tests/unit/test_zone.py +18 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
# Lower and upper bounds of the supported range (requires-python >=3.9).
|
|
14
|
+
python-version: ["3.9", "3.13"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install package with dev dependencies
|
|
21
|
+
run: pip install -e '.[dev]'
|
|
22
|
+
- name: Lint
|
|
23
|
+
run: ruff check src tests
|
|
24
|
+
- name: Type-check
|
|
25
|
+
run: mypy src
|
|
26
|
+
- name: Unit tests
|
|
27
|
+
run: pytest tests/unit -q
|
|
28
|
+
# Integration tiers (tests/integration) need live tenant credentials and
|
|
29
|
+
# are deliberately NOT run in CI — see docs/live-test-plan.md.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI on version tags (v0.2.0, v1.0.0, ...).
|
|
4
|
+
#
|
|
5
|
+
# INERT UNTIL CONFIGURED: this uses PyPI "trusted publishing" (no API token in
|
|
6
|
+
# the repo). Before the first release, register this repository + workflow as a
|
|
7
|
+
# trusted publisher for the 'exoscale-connector' project on pypi.org, and create
|
|
8
|
+
# the 'pypi' environment in the GitHub repo settings.
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
tags: ["v*"]
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
build:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
- name: Run the full check suite before building
|
|
23
|
+
run: |
|
|
24
|
+
pip install -e '.[dev]'
|
|
25
|
+
ruff check src tests
|
|
26
|
+
mypy src
|
|
27
|
+
pytest tests/unit -q
|
|
28
|
+
- name: Build sdist and wheel
|
|
29
|
+
run: |
|
|
30
|
+
pip install build
|
|
31
|
+
python -m build
|
|
32
|
+
- uses: actions/upload-artifact@v7
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
needs: build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment: pypi
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write # required for PyPI trusted publishing
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/download-artifact@v8
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: dist/
|
|
48
|
+
# Pinned to a commit SHA: this third-party action receives the OIDC
|
|
49
|
+
# token that authorizes publishing; a floating branch ref would let a
|
|
50
|
+
# compromised upstream publish on our behalf. Bump deliberately.
|
|
51
|
+
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 as of 2026-02-18
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Python build / cache
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# Local virtualenvs
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# Test / tooling caches
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.coverage
|
|
19
|
+
coverage.xml
|
|
20
|
+
htmlcov/
|
|
21
|
+
|
|
22
|
+
# Secrets & credentials — NEVER commit
|
|
23
|
+
# App reads credentials from environment variables only; injected at runtime.
|
|
24
|
+
.env
|
|
25
|
+
.env.*
|
|
26
|
+
*.env
|
|
27
|
+
secrets.json
|
|
28
|
+
secrets.yaml
|
|
29
|
+
secrets.yml
|
|
30
|
+
credentials
|
|
31
|
+
credentials.json
|
|
32
|
+
*.token
|
|
33
|
+
*.secret
|
|
34
|
+
|
|
35
|
+
# Exoscale / cloud credentials
|
|
36
|
+
# EXOSCALE_API_KEY / EXOSCALE_API_SECRET must come from the environment, not files.
|
|
37
|
+
exoscale.toml
|
|
38
|
+
.exoscale/
|
|
39
|
+
# Object Storage (SOS) uses boto3 — keep AWS-style shared creds out of the repo
|
|
40
|
+
.aws/
|
|
41
|
+
.boto
|
|
42
|
+
|
|
43
|
+
# Keys & certificates
|
|
44
|
+
*.pem
|
|
45
|
+
*.key
|
|
46
|
+
*.p12
|
|
47
|
+
*.pfx
|
|
48
|
+
*.crt
|
|
49
|
+
*.cer
|
|
50
|
+
# SSH keypairs (the ssh-key asset type can generate ephemeral ed25519 keys)
|
|
51
|
+
id_rsa
|
|
52
|
+
id_rsa.pub
|
|
53
|
+
id_ed25519
|
|
54
|
+
id_ed25519.pub
|
|
55
|
+
id_dsa
|
|
56
|
+
*.ppk
|
|
57
|
+
|
|
58
|
+
# Kubernetes — SKS generate_kubeconfig() output must never be committed
|
|
59
|
+
*.kubeconfig
|
|
60
|
+
kubeconfig
|
|
61
|
+
.kube/
|
|
62
|
+
|
|
63
|
+
# Terraform / IaC state & vars (this connector is built for IaC pipelines)
|
|
64
|
+
*.tfvars
|
|
65
|
+
*.tfstate
|
|
66
|
+
*.tfstate.*
|
|
67
|
+
.terraform/
|
|
68
|
+
|
|
69
|
+
# IDE / editor
|
|
70
|
+
.idea/
|
|
71
|
+
.vscode/
|
|
72
|
+
*.swp
|
|
73
|
+
*.swo
|
|
74
|
+
*~
|
|
75
|
+
|
|
76
|
+
# OS
|
|
77
|
+
.DS_Store
|
|
78
|
+
Thumbs.db
|
|
79
|
+
|
|
80
|
+
# Logs
|
|
81
|
+
*.log
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raphael Lang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: exoscale-connector
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A clean, typed, reusable Python connector for the Exoscale APIv2 — no CLI tool required.
|
|
5
|
+
Author: Raphael Lang
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: api,cloud,connector,exoscale,iac
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Requires-Dist: pydantic<3,>=2.5
|
|
14
|
+
Requires-Dist: requests>=2.28
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: cryptography>=40; extra == 'dev'
|
|
17
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
19
|
+
Requires-Dist: responses>=0.24; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
21
|
+
Requires-Dist: types-requests; extra == 'dev'
|
|
22
|
+
Provides-Extra: sos
|
|
23
|
+
Requires-Dist: boto3>=1.28; extra == 'sos'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# exoscale-connector
|
|
27
|
+
|
|
28
|
+
A clean, typed, reusable Python connector for the **Exoscale APIv2**. It talks to
|
|
29
|
+
the HTTP API directly — **no `exo` CLI and no Ansible required** — so it can be
|
|
30
|
+
dropped into any project that needs to read or manage Exoscale resources
|
|
31
|
+
programmatically.
|
|
32
|
+
|
|
33
|
+
- **Typed** — every request/response is a [pydantic](https://docs.pydantic.dev) v2
|
|
34
|
+
model, so you get validation and editor autocompletion.
|
|
35
|
+
- **One module per asset type** — `security-group`, `instance`, `elastic-ip`,
|
|
36
|
+
`dns`, `dbaas`, `sks`, … each with a small, uniform client.
|
|
37
|
+
- **Library + CLI** — import it, or use the per-asset command-line tools
|
|
38
|
+
(also namespaced under one `exoscale-connector` binary, with `--output table`).
|
|
39
|
+
- **Built for automation** — idempotent `ensure()` (get-or-create by name),
|
|
40
|
+
`wait_for_state` polling, and label-filtered listing make provisioning
|
|
41
|
+
scripts re-runnable by construction.
|
|
42
|
+
- **Self-contained** — runtime deps are just `requests` + `pydantic`; copy the
|
|
43
|
+
package into another repo and it keeps working.
|
|
44
|
+
- **Secret-safe** — credentials come only from the environment; nothing is
|
|
45
|
+
hardcoded or read from disk, and credentials are masked in `repr()`/log
|
|
46
|
+
output.
|
|
47
|
+
|
|
48
|
+
## Relationship to the official Exoscale SDK
|
|
49
|
+
|
|
50
|
+
Exoscale publishes an official, actively maintained Python SDK —
|
|
51
|
+
[`python-exoscale`](https://github.com/exoscale/python-exoscale) (the `exoscale`
|
|
52
|
+
package on PyPI); if you want the vendor-supported, batteries-included bindings,
|
|
53
|
+
use that. This connector is a smaller, opinionated alternative built for one
|
|
54
|
+
goal — a **drop-in, IaC-ready APIv2 client that is as easy to use as possible**:
|
|
55
|
+
where the official SDK splits into a high-level interface that grew up around the
|
|
56
|
+
now-retired APIv1 and a lower-level, OpenAPI-generated `exoscale.api.v2.Client`,
|
|
57
|
+
this project gives *every* asset type the same uniform, pydantic-typed client
|
|
58
|
+
plus a matching per-asset CLI, talks only to APIv2, reads credentials only from
|
|
59
|
+
the environment, polls async operations to completion, backs every asset type
|
|
60
|
+
with a live test that has run end-to-end against a real account, depends on just
|
|
61
|
+
`requests` + `pydantic`, and can be vendored by copying one folder.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e ".[dev]" # from this folder, for development
|
|
67
|
+
# or, once published / vendored:
|
|
68
|
+
pip install exoscale-connector
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Object Storage (S3-compatible) support pulls in `boto3`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install "exoscale-connector[sos]"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quickstart (library)
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from exoscale_connector import ExoscaleClient
|
|
81
|
+
from exoscale_connector.resources.security_group import (
|
|
82
|
+
SecurityGroupClient, SecurityGroupRule,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Credentials from EXOSCALE_API_KEY / EXOSCALE_API_SECRET in the environment.
|
|
86
|
+
client = ExoscaleClient.from_env(zone="de-fra-1")
|
|
87
|
+
sg = SecurityGroupClient(client)
|
|
88
|
+
|
|
89
|
+
for group in sg.list():
|
|
90
|
+
print(group.id, group.name)
|
|
91
|
+
|
|
92
|
+
group = sg.create({"name": "web", "description": "public web tier"})
|
|
93
|
+
sg.add_rule(group.id, SecurityGroupRule(
|
|
94
|
+
flow_direction="ingress", protocol="tcp",
|
|
95
|
+
start_port=443, end_port=443, network="0.0.0.0/0",
|
|
96
|
+
))
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Quickstart (CLI)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
export EXOSCALE_API_KEY=... EXOSCALE_API_SECRET=... EXOSCALE_ZONE=de-fra-1
|
|
103
|
+
|
|
104
|
+
exoscale-security-group list
|
|
105
|
+
exoscale-security-group get --id <uuid>
|
|
106
|
+
exoscale-security-group create --json '{"name": "web"}'
|
|
107
|
+
exoscale-security-group delete --id <uuid>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> In practice, inject the credentials with your secret-management tooling rather
|
|
111
|
+
> than exporting them by hand. The connector only reads environment variables, so
|
|
112
|
+
> any injector works (HashiCorp Vault, Infisical, Doppler, …), e.g.
|
|
113
|
+
> `<vault-cli> run -- exoscale-security-group list`.
|
|
114
|
+
|
|
115
|
+
## Documentation
|
|
116
|
+
|
|
117
|
+
- **[User / operator guide](docs/user-guide.md)** — installing, authenticating,
|
|
118
|
+
zones, and the common commands shared by every asset type.
|
|
119
|
+
- **[Asset type reference](docs/asset-types/README.md)** — one page per asset
|
|
120
|
+
type with model schema, CLI subcommands, library snippets, gotchas, and a
|
|
121
|
+
runnable end-to-end example backed by a passing live test.
|
|
122
|
+
- **[IAM policy cookbook](docs/iam-policy-cookbook.md)** — helper constructors
|
|
123
|
+
and copy-paste recipes for IAM role policies (the one area with real depth).
|
|
124
|
+
- **[Developer guide](docs/developer-guide.md)** — architecture, how to add a
|
|
125
|
+
new asset type, and the testing strategy.
|
|
126
|
+
- **[Live test plan](docs/live-test-plan.md)** — tiered per-asset live-test
|
|
127
|
+
design (safety rails, naming prefix, cleanup invariants, cost model).
|
|
128
|
+
- **[Live test results](docs/live-test-results.md)** — run log of every live
|
|
129
|
+
test executed against a real Exoscale tenant, plus the bugs each tier
|
|
130
|
+
surfaced and how they were fixed.
|
|
131
|
+
|
|
132
|
+
Every asset type the connector supports has a live test that has actually run
|
|
133
|
+
end-to-end against a real account; the gotchas in the asset-type pages are
|
|
134
|
+
empirical, not theoretical.
|
|
135
|
+
|
|
136
|
+
## Maintenance & support
|
|
137
|
+
|
|
138
|
+
This is a personal project, maintained on a **best-effort, occasional basis** —
|
|
139
|
+
not full-time and not on a fixed schedule. It's shared because it may be useful
|
|
140
|
+
to others, not as a supported product. Issues and pull requests are welcome and
|
|
141
|
+
will be looked at when time allows, but there is **no guaranteed response time or
|
|
142
|
+
release cadence**. The API surface it tracks can drift; if you depend on it,
|
|
143
|
+
pin a version and don't hesitate to fork and adapt it — that's encouraged.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
Released under the [MIT License](LICENSE) — free to use, modify, and
|
|
148
|
+
redistribute, including commercially. Provided **as-is, without warranty of any
|
|
149
|
+
kind**; use entirely at your own risk. The only condition is that the copyright
|
|
150
|
+
and permission notice are kept in copies.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# exoscale-connector
|
|
2
|
+
|
|
3
|
+
A clean, typed, reusable Python connector for the **Exoscale APIv2**. It talks to
|
|
4
|
+
the HTTP API directly — **no `exo` CLI and no Ansible required** — so it can be
|
|
5
|
+
dropped into any project that needs to read or manage Exoscale resources
|
|
6
|
+
programmatically.
|
|
7
|
+
|
|
8
|
+
- **Typed** — every request/response is a [pydantic](https://docs.pydantic.dev) v2
|
|
9
|
+
model, so you get validation and editor autocompletion.
|
|
10
|
+
- **One module per asset type** — `security-group`, `instance`, `elastic-ip`,
|
|
11
|
+
`dns`, `dbaas`, `sks`, … each with a small, uniform client.
|
|
12
|
+
- **Library + CLI** — import it, or use the per-asset command-line tools
|
|
13
|
+
(also namespaced under one `exoscale-connector` binary, with `--output table`).
|
|
14
|
+
- **Built for automation** — idempotent `ensure()` (get-or-create by name),
|
|
15
|
+
`wait_for_state` polling, and label-filtered listing make provisioning
|
|
16
|
+
scripts re-runnable by construction.
|
|
17
|
+
- **Self-contained** — runtime deps are just `requests` + `pydantic`; copy the
|
|
18
|
+
package into another repo and it keeps working.
|
|
19
|
+
- **Secret-safe** — credentials come only from the environment; nothing is
|
|
20
|
+
hardcoded or read from disk, and credentials are masked in `repr()`/log
|
|
21
|
+
output.
|
|
22
|
+
|
|
23
|
+
## Relationship to the official Exoscale SDK
|
|
24
|
+
|
|
25
|
+
Exoscale publishes an official, actively maintained Python SDK —
|
|
26
|
+
[`python-exoscale`](https://github.com/exoscale/python-exoscale) (the `exoscale`
|
|
27
|
+
package on PyPI); if you want the vendor-supported, batteries-included bindings,
|
|
28
|
+
use that. This connector is a smaller, opinionated alternative built for one
|
|
29
|
+
goal — a **drop-in, IaC-ready APIv2 client that is as easy to use as possible**:
|
|
30
|
+
where the official SDK splits into a high-level interface that grew up around the
|
|
31
|
+
now-retired APIv1 and a lower-level, OpenAPI-generated `exoscale.api.v2.Client`,
|
|
32
|
+
this project gives *every* asset type the same uniform, pydantic-typed client
|
|
33
|
+
plus a matching per-asset CLI, talks only to APIv2, reads credentials only from
|
|
34
|
+
the environment, polls async operations to completion, backs every asset type
|
|
35
|
+
with a live test that has run end-to-end against a real account, depends on just
|
|
36
|
+
`requests` + `pydantic`, and can be vendored by copying one folder.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e ".[dev]" # from this folder, for development
|
|
42
|
+
# or, once published / vendored:
|
|
43
|
+
pip install exoscale-connector
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Object Storage (S3-compatible) support pulls in `boto3`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install "exoscale-connector[sos]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quickstart (library)
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from exoscale_connector import ExoscaleClient
|
|
56
|
+
from exoscale_connector.resources.security_group import (
|
|
57
|
+
SecurityGroupClient, SecurityGroupRule,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Credentials from EXOSCALE_API_KEY / EXOSCALE_API_SECRET in the environment.
|
|
61
|
+
client = ExoscaleClient.from_env(zone="de-fra-1")
|
|
62
|
+
sg = SecurityGroupClient(client)
|
|
63
|
+
|
|
64
|
+
for group in sg.list():
|
|
65
|
+
print(group.id, group.name)
|
|
66
|
+
|
|
67
|
+
group = sg.create({"name": "web", "description": "public web tier"})
|
|
68
|
+
sg.add_rule(group.id, SecurityGroupRule(
|
|
69
|
+
flow_direction="ingress", protocol="tcp",
|
|
70
|
+
start_port=443, end_port=443, network="0.0.0.0/0",
|
|
71
|
+
))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Quickstart (CLI)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export EXOSCALE_API_KEY=... EXOSCALE_API_SECRET=... EXOSCALE_ZONE=de-fra-1
|
|
78
|
+
|
|
79
|
+
exoscale-security-group list
|
|
80
|
+
exoscale-security-group get --id <uuid>
|
|
81
|
+
exoscale-security-group create --json '{"name": "web"}'
|
|
82
|
+
exoscale-security-group delete --id <uuid>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
> In practice, inject the credentials with your secret-management tooling rather
|
|
86
|
+
> than exporting them by hand. The connector only reads environment variables, so
|
|
87
|
+
> any injector works (HashiCorp Vault, Infisical, Doppler, …), e.g.
|
|
88
|
+
> `<vault-cli> run -- exoscale-security-group list`.
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
- **[User / operator guide](docs/user-guide.md)** — installing, authenticating,
|
|
93
|
+
zones, and the common commands shared by every asset type.
|
|
94
|
+
- **[Asset type reference](docs/asset-types/README.md)** — one page per asset
|
|
95
|
+
type with model schema, CLI subcommands, library snippets, gotchas, and a
|
|
96
|
+
runnable end-to-end example backed by a passing live test.
|
|
97
|
+
- **[IAM policy cookbook](docs/iam-policy-cookbook.md)** — helper constructors
|
|
98
|
+
and copy-paste recipes for IAM role policies (the one area with real depth).
|
|
99
|
+
- **[Developer guide](docs/developer-guide.md)** — architecture, how to add a
|
|
100
|
+
new asset type, and the testing strategy.
|
|
101
|
+
- **[Live test plan](docs/live-test-plan.md)** — tiered per-asset live-test
|
|
102
|
+
design (safety rails, naming prefix, cleanup invariants, cost model).
|
|
103
|
+
- **[Live test results](docs/live-test-results.md)** — run log of every live
|
|
104
|
+
test executed against a real Exoscale tenant, plus the bugs each tier
|
|
105
|
+
surfaced and how they were fixed.
|
|
106
|
+
|
|
107
|
+
Every asset type the connector supports has a live test that has actually run
|
|
108
|
+
end-to-end against a real account; the gotchas in the asset-type pages are
|
|
109
|
+
empirical, not theoretical.
|
|
110
|
+
|
|
111
|
+
## Maintenance & support
|
|
112
|
+
|
|
113
|
+
This is a personal project, maintained on a **best-effort, occasional basis** —
|
|
114
|
+
not full-time and not on a fixed schedule. It's shared because it may be useful
|
|
115
|
+
to others, not as a supported product. Issues and pull requests are welcome and
|
|
116
|
+
will be looked at when time allows, but there is **no guaranteed response time or
|
|
117
|
+
release cadence**. The API surface it tracks can drift; if you depend on it,
|
|
118
|
+
pin a version and don't hesitate to fork and adapt it — that's encouraged.
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
Released under the [MIT License](LICENSE) — free to use, modify, and
|
|
123
|
+
redistribute, including commercially. Provided **as-is, without warranty of any
|
|
124
|
+
kind**; use entirely at your own risk. The only condition is that the copyright
|
|
125
|
+
and permission notice are kept in copies.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Asset type reference
|
|
2
|
+
|
|
3
|
+
One page per asset type the connector supports. Every page has the same six
|
|
4
|
+
sections — **Overview**, **Model**, **CLI**, **Library**, **Gotchas**,
|
|
5
|
+
**End-to-end example** — and is backed by a passing
|
|
6
|
+
[live test](../live-test-plan.md).
|
|
7
|
+
|
|
8
|
+
If something on a page contradicts the live behaviour, the live test is the
|
|
9
|
+
source of truth — open an issue and the page will be corrected.
|
|
10
|
+
|
|
11
|
+
## Capability matrix
|
|
12
|
+
|
|
13
|
+
| Asset type | CLI binary | Live-tested | Tier |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| [security-group (+rules)](security-group.md) | `exoscale-security-group` | ✅ | 1 |
|
|
16
|
+
| [private-network](private-network.md) | `exoscale-private-network` | ✅ | 1 |
|
|
17
|
+
| [anti-affinity-group](anti-affinity-group.md) | `exoscale-anti-affinity-group` | ✅ | 1 |
|
|
18
|
+
| [ssh-key](ssh-key.md) | `exoscale-ssh-key` | ✅ | 1 |
|
|
19
|
+
| [iam-role](iam-role.md) | `exoscale-iam-role` | ✅ | 1 |
|
|
20
|
+
| [iam-user](iam-user.md) | `exoscale-iam-user` | read-only | — |
|
|
21
|
+
| [api-key](api-key.md) | `exoscale-api-key` | ✅ (gated) | 1 (opt-in, `EXOSCALE_TEST_TIER_1_API_KEY=1`) |
|
|
22
|
+
| [dns (domain + records)](dns.md) | `exoscale-dns` | ✅ | 1 |
|
|
23
|
+
| [elastic-ip](elastic-ip.md) | `exoscale-elastic-ip` | ✅ | 2 |
|
|
24
|
+
| [object-storage bucket](object-storage.md) | `exoscale-bucket` | ✅ | 2 |
|
|
25
|
+
| [block-volume](block-volume.md) | `exoscale-block-volume` | ✅ create/snapshot/delete (Tier 2); attach/detach (Tier 3); resize endpoint+method verified, size-change assertion self-skips on tenant quota | 2/3 |
|
|
26
|
+
| [block-volume-snapshot](block-volume-snapshot.md) | `exoscale-block-volume-snapshot` | ✅ | 2 |
|
|
27
|
+
| [instance (+lifecycle)](instance.md) | `exoscale-instance` | ✅ | 3 |
|
|
28
|
+
| [instance-pool (+scale)](instance-pool.md) | `exoscale-instance-pool` | ✅ | 3 |
|
|
29
|
+
| [snapshot (compute)](snapshot.md) | `exoscale-snapshot` | ✅ create/list/get/export/delete | 3 |
|
|
30
|
+
| [load-balancer (+services)](load-balancer.md) | `exoscale-load-balancer` | ✅ | 4 |
|
|
31
|
+
| [dbaas](dbaas.md) | `exoscale-dbaas` | ✅ | 4 |
|
|
32
|
+
| [sks (cluster + nodepool)](sks.md) | `exoscale-sks` | ✅ | 4 |
|
|
33
|
+
| [zone](zone.md) | `exoscale-zone` | pending (smoke test added) | 0 |
|
|
34
|
+
| [template](template.md) | `exoscale-template` | pending (smoke test added) | 0 |
|
|
35
|
+
| [instance-type](instance-type.md) | `exoscale-instance-type` | pending (smoke test added) | 0 |
|
|
36
|
+
|
|
37
|
+
Instance scale, reverse DNS, SOS objects, DBaaS users/update, and `ensure()`
|
|
38
|
+
were all live-verified in the 2026-06-10 extensions validation run (see
|
|
39
|
+
[live-test-results.md](../live-test-results.md)). Three spec-vs-reality
|
|
40
|
+
divergences were found and fixed during that run.
|
|
41
|
+
|
|
42
|
+
## Page template
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
# <asset-type>
|
|
46
|
+
Overview — one paragraph.
|
|
47
|
+
## Model
|
|
48
|
+
Field table from the pydantic model.
|
|
49
|
+
## CLI
|
|
50
|
+
Every subcommand with a copy-pasteable example invocation.
|
|
51
|
+
## Library
|
|
52
|
+
Python snippet for each operation.
|
|
53
|
+
## Gotchas
|
|
54
|
+
Caveats verified by the live test (e.g. unit-of-measure quirks,
|
|
55
|
+
required-but-undocumented fields, quota constraints).
|
|
56
|
+
## End-to-end example
|
|
57
|
+
The full lifecycle distilled from the corresponding live test.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Conventions used on every page
|
|
61
|
+
|
|
62
|
+
- **Authentication** is always env-based: `EXOSCALE_API_KEY` /
|
|
63
|
+
`EXOSCALE_API_SECRET` / `EXOSCALE_ZONE`. Inject with your secret manager
|
|
64
|
+
(HashiCorp Vault, Infisical, Doppler, …); the connector reads only env vars.
|
|
65
|
+
- **JSON output** from CLIs goes to stdout; errors to stderr; exit 0 on
|
|
66
|
+
success, 1 on API/connector error, 2 on argparse error.
|
|
67
|
+
- **All resources** are pydantic v2 models with snake_case attributes that
|
|
68
|
+
auto-map to the API's kebab-case JSON keys (e.g. `flow_direction` ↔
|
|
69
|
+
`flow-direction`). Unknown server fields pass through (`extra="allow"`),
|
|
70
|
+
so the connector keeps working when the API adds fields ahead of the model.
|
|
71
|
+
- **Async operations** are awaited by default — pass `wait=False` to return
|
|
72
|
+
the operation object without polling.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# anti-affinity-group
|
|
2
|
+
|
|
3
|
+
A scheduling hint telling Exoscale's placement engine that the instances
|
|
4
|
+
assigned to this group should be spread across distinct physical hosts. Used
|
|
5
|
+
to maximise availability for replica sets (e.g. a 3-node etcd cluster, or an
|
|
6
|
+
HA database).
|
|
7
|
+
|
|
8
|
+
## Model
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
class AntiAffinityGroup(ExoscaleModel):
|
|
12
|
+
id: Optional[str]
|
|
13
|
+
name: Optional[str]
|
|
14
|
+
description: Optional[str]
|
|
15
|
+
instances: Optional[List[Reference]] # members; populated on detail responses
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## CLI
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
exoscale-anti-affinity-group list
|
|
22
|
+
exoscale-anti-affinity-group get --id <uuid>
|
|
23
|
+
exoscale-anti-affinity-group find --name <name>
|
|
24
|
+
exoscale-anti-affinity-group create --json '{"name": "etcd-aag", "description": "etcd replica anti-affinity"}'
|
|
25
|
+
exoscale-anti-affinity-group delete --id <uuid>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Library
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from exoscale_connector import ExoscaleClient
|
|
32
|
+
from exoscale_connector.resources.anti_affinity_group import AntiAffinityGroupClient
|
|
33
|
+
|
|
34
|
+
aag = AntiAffinityGroupClient(ExoscaleClient.from_env(zone="de-fra-1"))
|
|
35
|
+
|
|
36
|
+
group = aag.create({"name": "etcd-aag", "description": "etcd anti-affinity"})
|
|
37
|
+
fetched = aag.get(group.id)
|
|
38
|
+
aag.delete(group.id)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Gotchas
|
|
42
|
+
|
|
43
|
+
- **No `update` endpoint.** The API does not support modifying an AAG in
|
|
44
|
+
place — `update()` is intentionally not exposed on this client. To change
|
|
45
|
+
anything, delete and recreate.
|
|
46
|
+
- **Members are read-only here.** Instances are assigned via the
|
|
47
|
+
*instance* create/update endpoint by including the AAG id in the
|
|
48
|
+
instance's `anti-affinity-groups` array.
|
|
49
|
+
|
|
50
|
+
## End-to-end example
|
|
51
|
+
|
|
52
|
+
Distilled from
|
|
53
|
+
[`tests/integration/test_tier_1.py::test_anti_affinity_group_lifecycle`](../../tests/integration/test_tier_1.py):
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from exoscale_connector import ExoscaleClient
|
|
57
|
+
from exoscale_connector.resources.anti_affinity_group import AntiAffinityGroupClient
|
|
58
|
+
|
|
59
|
+
aag = AntiAffinityGroupClient(ExoscaleClient.from_env(zone="de-fra-1"))
|
|
60
|
+
|
|
61
|
+
group = aag.create({"name": "etcd-aag", "description": "tier-1 smoke"})
|
|
62
|
+
assert aag.get(group.id).name == "etcd-aag"
|
|
63
|
+
assert aag.find_by_name("etcd-aag").id == group.id
|
|
64
|
+
aag.delete(group.id)
|
|
65
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# api-key
|
|
2
|
+
|
|
3
|
+
A scoped credential bound to an IAM role at creation time. The role
|
|
4
|
+
determines what the key can do; the key's name/identifier on the wire is
|
|
5
|
+
its **key id** (an API-generated string), and the **secret is returned
|
|
6
|
+
exactly once** on the create response — there is no way to re-fetch it.
|
|
7
|
+
|
|
8
|
+
## Model
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
class ApiKey(ExoscaleModel):
|
|
12
|
+
key: Optional[str] # API key id (the public part, used in URL paths)
|
|
13
|
+
name: Optional[str]
|
|
14
|
+
role_id: Optional[str] # bound role's id
|
|
15
|
+
role: Optional[Reference]
|
|
16
|
+
secret: Optional[str] # PRESENT ONLY ON CREATE — never returned later
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## CLI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
exoscale-api-key list
|
|
23
|
+
exoscale-api-key get --id <key-id>
|
|
24
|
+
exoscale-api-key find --name <name>
|
|
25
|
+
exoscale-api-key create --json '{"name": "ci-bot", "role-id": "<role-uuid>"}'
|
|
26
|
+
exoscale-api-key delete --id <key-id>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Library
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from exoscale_connector import ExoscaleClient
|
|
33
|
+
from exoscale_connector.resources.api_key import ApiKeyClient
|
|
34
|
+
|
|
35
|
+
keys = ApiKeyClient(ExoscaleClient.from_env(zone="de-fra-1"))
|
|
36
|
+
|
|
37
|
+
created = keys.create({"name": "ci-bot", "role-id": "<role-uuid>"})
|
|
38
|
+
# Capture the secret IMMEDIATELY — it is not retrievable later.
|
|
39
|
+
secret = created.secret
|
|
40
|
+
key_id = created.key
|
|
41
|
+
|
|
42
|
+
# Listing / fetching never returns the secret.
|
|
43
|
+
for existing in keys.list():
|
|
44
|
+
print(existing.key, existing.name)
|
|
45
|
+
|
|
46
|
+
keys.delete(key_id)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Gotchas
|
|
50
|
+
|
|
51
|
+
- **`secret` is one-shot.** The first response after `create` is the only
|
|
52
|
+
time the secret is visible. Persist it to a vault immediately; never log it.
|
|
53
|
+
As a guard, `repr()` of an `ApiKey` masks the secret — but it is still
|
|
54
|
+
present in `model_dump()` and in the CLI's `create` JSON output (that is
|
|
55
|
+
your one chance to capture it).
|
|
56
|
+
- **`id_field = "key"`** — the path token is the `key` field, not a uuid in
|
|
57
|
+
`id`. `keys.get("EXO...")` calls `GET /api-key/EXO...`.
|
|
58
|
+
- **`update` not exposed.** The API does not allow rotating a key in place —
|
|
59
|
+
delete and create a new one.
|
|
60
|
+
- **Tier 1 live test for create is gated separately** (off by default) so
|
|
61
|
+
the standard Tier 1 run never produces a stray secret-bearing response.
|
|
62
|
+
Enable with `EXOSCALE_TEST_TIER_1_API_KEY=1`.
|
|
63
|
+
|
|
64
|
+
## End-to-end example
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from exoscale_connector import ExoscaleClient
|
|
68
|
+
from exoscale_connector.resources.api_key import ApiKeyClient
|
|
69
|
+
from exoscale_connector.resources.iam_role import IAMPolicy, IAMRole, IAMRoleClient
|
|
70
|
+
|
|
71
|
+
client = ExoscaleClient.from_env(zone="de-fra-1")
|
|
72
|
+
roles = IAMRoleClient(client)
|
|
73
|
+
keys = ApiKeyClient(client)
|
|
74
|
+
|
|
75
|
+
# Bind to a minimal role created just for this key.
|
|
76
|
+
role = roles.create(IAMRole(
|
|
77
|
+
name="ci-bot-role",
|
|
78
|
+
policy=IAMPolicy(default_service_strategy="deny", services={}),
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
created = keys.create({"name": "ci-bot", "role-id": role.id})
|
|
82
|
+
assert created.secret, "secret must be returned exactly once"
|
|
83
|
+
# Hand `created.key` / `created.secret` to your vault here.
|
|
84
|
+
|
|
85
|
+
# Teardown
|
|
86
|
+
keys.delete(created.key)
|
|
87
|
+
roles.delete(role.id)
|
|
88
|
+
```
|