portainer-mcp/test_gitops_create.py
Adolfo Delorenzo d7055a912e Fix: Add GitOps support during stack creation and document API limitations
- Updated portainer_stacks_server.py to support GitOps configuration during stack creation
- Added enable_gitops parameter and related GitOps settings to create_compose_stack_from_git
- Created comprehensive documentation (README_GITOPS.md) explaining:
  - Why GitOps cannot be enabled on existing stacks
  - Why GitOps intervals cannot be updated without recreating stacks
  - API limitations that cause Git-based stacks to detach when updated
- Added test script (test_gitops_create.py) to verify GitOps functionality
- Included portainer_gitops_server.py for reference

The Portainer API requires StackFileContent for updates, which detaches stacks from Git.
This is a fundamental API limitation, not an MCP implementation issue.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 23:16:08 -03:00

164 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""Test creating a stack with GitOps enabled"""
import asyncio
import aiohttp
import json
import sys
# Configuration
PORTAINER_URL = "https://partner.portainer.live"
PORTAINER_API_KEY = "ptr_uMqreULEo44qvuszgG8oZWdjkDx3K9HBXSmjd+F/vDE="
# Stack configuration
STACK_NAME = "nginx03-gitops"
ENVIRONMENT_ID = 6 # docker03
REPOSITORY_URL = "https://git.oe74.net/adelorenzo/portainer-yaml"
REPOSITORY_REF = "main"
COMPOSE_PATH = "docker-compose.yml"
async def create_stack_with_gitops():
"""Create a stack with GitOps enabled from the start"""
# Build request data
data = {
"name": STACK_NAME,
"repositoryURL": REPOSITORY_URL,
"repositoryReferenceName": REPOSITORY_REF,
"composeFilePathInRepository": COMPOSE_PATH,
"repositoryAuthentication": False,
"autoUpdate": {
"interval": "5m",
"forcePullImage": True,
"forceUpdate": False
}
}
# Headers
headers = {
"X-API-Key": PORTAINER_API_KEY,
"Content-Type": "application/json"
}
# API endpoint
endpoint = f"{PORTAINER_URL}/api/stacks/create/standalone/repository?endpointId={ENVIRONMENT_ID}"
print(f"Creating stack '{STACK_NAME}' with GitOps enabled...")
print(f"Repository: {REPOSITORY_URL}")
print(f"Compose file: {COMPOSE_PATH}")
print(f"Environment ID: {ENVIRONMENT_ID}")
print(f"GitOps: Enabled (polling every 5m)")
try:
async with aiohttp.ClientSession() as session:
async with session.post(endpoint, json=data, headers=headers) as response:
response_text = await response.text()
if response.status in [200, 201]:
result = json.loads(response_text)
print(f"\n✅ Stack created successfully!")
print(f"Stack ID: {result['Id']}")
print(f"Stack Name: {result['Name']}")
# Check GitOps status
if result.get("AutoUpdate"):
print(f"\n🔄 GitOps Status:")
auto_update = result["AutoUpdate"]
print(f" Enabled: Yes")
print(f" Interval: {auto_update.get('Interval', 'N/A')}")
print(f" Force Pull Image: {auto_update.get('ForcePullImage', False)}")
print(f" Force Update: {auto_update.get('ForceUpdate', False)}")
else:
print(f"\n⚠️ GitOps is not enabled on the created stack")
return result
else:
print(f"\n❌ Error creating stack: {response.status}")
print(f"Response: {response_text}")
# Try to parse error message
try:
error_data = json.loads(response_text)
if "message" in error_data:
print(f"Error message: {error_data['message']}")
elif "details" in error_data:
print(f"Error details: {error_data['details']}")
except:
pass
return None
except Exception as e:
print(f"\n❌ Exception occurred: {str(e)}")
return None
async def check_stack_gitops(stack_id):
"""Check if GitOps is enabled on a stack"""
headers = {
"X-API-Key": PORTAINER_API_KEY
}
endpoint = f"{PORTAINER_URL}/api/stacks/{stack_id}"
try:
async with aiohttp.ClientSession() as session:
async with session.get(endpoint, headers=headers) as response:
if response.status == 200:
result = await response.json()
print(f"\n📚 Stack Details:")
print(f"Name: {result['Name']}")
print(f"ID: {result['Id']}")
if result.get("GitConfig"):
print(f"\n🔗 Git Configuration:")
git = result["GitConfig"]
print(f" Repository: {git['URL']}")
print(f" Reference: {git['ReferenceName']}")
print(f" Path: {git.get('ConfigFilePath', 'N/A')}")
if result.get("AutoUpdate"):
print(f"\n🔄 GitOps Configuration:")
auto = result["AutoUpdate"]
print(f" Enabled: Yes")
print(f" Interval: {auto.get('Interval', 'N/A')}")
print(f" Force Pull Image: {auto.get('ForcePullImage', False)}")
print(f" Force Update: {auto.get('ForceUpdate', False)}")
if auto.get("Webhook"):
print(f" Webhook: Enabled")
else:
print(f"\n❌ GitOps: Disabled")
return result
else:
print(f"Error getting stack details: {response.status}")
return None
except Exception as e:
print(f"Exception getting stack details: {str(e)}")
return None
async def main():
print("=" * 60)
print("Testing Portainer Stack Creation with GitOps")
print("=" * 60)
# Create the stack with GitOps
result = await create_stack_with_gitops()
if result:
print("\n" + "=" * 60)
print("Verifying GitOps configuration...")
print("=" * 60)
# Wait a moment for the stack to be fully created
await asyncio.sleep(2)
# Check the stack details
await check_stack_gitops(result["Id"])
print("\n🎉 Test completed!")
else:
print("\n😞 Test failed!")
if __name__ == "__main__":
asyncio.run(main())