Post-Tyranny-Tech-Infrastru.../ansible/roles/zitadel/files/setup_automation.py
Pieter 48ef4da920 Fix Zitadel deployment by removing FirstInstance variables
- Remove all ZITADEL_FIRSTINSTANCE_* environment variables
- Fixes migration error: duplicate key constraint violation
- Root cause: Bug in Zitadel v2.63.7 FirstInstance migration
- Workaround: Complete initial setup via web UI
- Upstream issue: https://github.com/zitadel/zitadel/issues/8791

Changes:
- Clean up obsolete documentation (OIDC_AUTOMATION.md, SETUP_GUIDE.md, COLLABORA_SETUP.md)
- Add PROJECT_REFERENCE.md for essential configuration info
- Add force recreate functionality with clean database volumes
- Update bootstrap instructions for web UI setup
- Document one-time manual setup requirement for OIDC automation

Zitadel now deploys successfully and is accessible at:
https://zitadel.test.vrije.cloud

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-06 16:43:57 +01:00

166 lines
5.1 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
One-time setup script to configure Zitadel for full OIDC automation.
This script uses the manually created PAT to set up everything needed.
"""
import json
import sys
import requests
from typing import Optional
class ZitadelSetup:
"""Setup Zitadel for OIDC automation."""
def __init__(self, domain: str, pat_token: str):
self.domain = domain
self.base_url = f"https://{domain}"
self.headers = {
"Authorization": f"Bearer {pat_token}",
"Content-Type": "application/json",
}
def create_project(self, name: str) -> Optional[str]:
"""Create a project for OIDC applications."""
url = f"{self.base_url}/management/v1/projects"
payload = {"name": name}
print(f"📦 Creating project '{name}'...")
response = requests.post(url, headers=self.headers, json=payload)
if response.status_code in [200, 201]:
project_id = response.json().get("id")
print(f"✅ Project created: {project_id}")
return project_id
elif response.status_code == 409:
print(f" Project already exists, searching...")
return self.find_project(name)
else:
print(f"❌ Failed to create project: {response.status_code}")
print(f"Response: {response.text}")
return None
def find_project(self, name: str) -> Optional[str]:
"""Find existing project by name."""
url = f"{self.base_url}/management/v1/projects/_search"
response = requests.post(url, headers=self.headers, json={})
if response.status_code == 200:
projects = response.json().get("result", [])
for project in projects:
if project.get("name") == name:
print(f"✅ Found existing project: {project['id']}")
return project["id"]
return None
def get_service_user_id(self, username: str) -> Optional[str]:
"""Find the service user ID."""
url = f"{self.base_url}/management/v1/users/_search"
payload = {
"query": {
"userName": username
}
}
print(f"🔍 Looking for service user '{username}'...")
response = requests.post(url, headers=self.headers, json=payload)
if response.status_code == 200:
users = response.json().get("result", [])
if users:
user_id = users[0].get("id")
print(f"✅ Found service user: {user_id}")
return user_id
print(f"❌ Service user not found")
return None
def grant_project_permission(self, project_id: str, user_id: str) -> bool:
"""Grant project ownership to service user."""
url = f"{self.base_url}/management/v1/projects/{project_id}/roles/_bulk/set"
# Grant PROJECT_OWNER role
payload = {
"grants": [
{
"userId": user_id,
"roleKeys": ["PROJECT_OWNER"]
}
]
}
print(f"🔐 Granting PROJECT_OWNER permission...")
response = requests.post(url, headers=self.headers, json=payload)
if response.status_code in [200, 201]:
print(f"✅ Permission granted")
return True
else:
print(f"⚠️ Permission grant: {response.status_code}")
print(f"Response: {response.text}")
# This might fail if already granted, which is OK
return True
def main():
if len(sys.argv) < 3:
print("Usage: setup_automation.py <domain> <pat_token>")
sys.exit(1)
domain = sys.argv[1]
pat_token = sys.argv[2]
print(f"""
🚀 Zitadel OIDC Automation Setup
=================================
Domain: {domain}
This script will:
1. Create 'SSO Applications' project
2. Grant api-automation user PROJECT_OWNER permission
3. Enable full OIDC automation
""")
try:
setup = ZitadelSetup(domain, pat_token)
# Step 1: Create or find project
project_id = setup.create_project("SSO Applications")
if not project_id:
print("\n❌ Failed to create/find project")
sys.exit(1)
# Step 2: Find service user
user_id = setup.get_service_user_id("api-automation")
if not user_id:
print("\n❌ Service user 'api-automation' not found")
print("Please create the machine user first via Zitadel console")
sys.exit(1)
# Step 3: Grant permissions
if not setup.grant_project_permission(project_id, user_id):
print("\n⚠️ Warning: Could not grant permissions (may already exist)")
print(f"""
✅ SUCCESS! OIDC automation is now fully configured.
Next steps:
- Run deployment: ansible-playbook -i hcloud.yml playbooks/deploy.yml
- All OIDC apps will be created automatically
- No more manual steps required!
Project ID: {project_id}
Service User: api-automation ({user_id})
""")
except Exception as e:
print(f"\n❌ Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()