ok... ok... I'll add it to git...
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
# Stack Automation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The `update-stack.sh` script enables programmatic stack updates via Portainer's REST API. This allows LLM agents (like Claude) and automation scripts to safely update Portainer stacks without requiring UI access.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Navigate to stacks directory
|
||||
cd /home/jpmschweitzer/Projects/portainer-core/stacks
|
||||
|
||||
# Update a stack (interactive mode - first time)
|
||||
./update-stack.sh open-webui.yml
|
||||
|
||||
# Subsequent updates (uses stored token)
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. **First Run:**
|
||||
- Prompts for Portainer username/password
|
||||
- Authenticates with Portainer API
|
||||
- Generates JWT access token
|
||||
- Saves token to `.portainer-token` (gitignored)
|
||||
|
||||
2. **Subsequent Runs:**
|
||||
- Reads token from `.portainer-token`
|
||||
- Uses token for API calls
|
||||
- No credential prompts needed
|
||||
|
||||
### Update Process
|
||||
|
||||
1. Reads YAML file from `stacks/` directory
|
||||
2. Authenticates with Portainer (or uses cached token)
|
||||
3. Looks up stack by name (filename without .yml)
|
||||
4. Sends updated stack configuration via API
|
||||
5. Portainer validates and applies changes
|
||||
|
||||
## Usage Modes
|
||||
|
||||
### Interactive Mode (Human Operators)
|
||||
|
||||
```bash
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
**First run prompts for:**
|
||||
- Portainer username
|
||||
- Portainer password
|
||||
|
||||
**Token persists for subsequent runs.**
|
||||
|
||||
### Non-Interactive Mode (Automation/LLM Agents)
|
||||
|
||||
```bash
|
||||
export PORTAINER_USERNAME="admin"
|
||||
export PORTAINER_PASSWORD="your-secure-password"
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
**Use this mode for:**
|
||||
- CI/CD pipelines
|
||||
- LLM agent workflows
|
||||
- Automated deployment scripts
|
||||
- Cron jobs
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `PORTAINER_URL` | No | `http://localhost:8080` | Portainer instance URL |
|
||||
| `PORTAINER_USERNAME` | Non-interactive only | - | Admin username |
|
||||
| `PORTAINER_PASSWORD` | Non-interactive only | - | Admin password |
|
||||
|
||||
## Examples
|
||||
|
||||
### Update Single Stack
|
||||
|
||||
```bash
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
### Update Multiple Stacks
|
||||
|
||||
```bash
|
||||
for stack in open-webui.yml ollama.yml core-api.yml; do
|
||||
./update-stack.sh "$stack"
|
||||
echo "---"
|
||||
done
|
||||
```
|
||||
|
||||
### LLM Agent Integration
|
||||
|
||||
```bash
|
||||
# Claude Code workflow example
|
||||
export PORTAINER_USERNAME="admin"
|
||||
export PORTAINER_PASSWORD="${PORTAINER_ADMIN_PASSWORD}" # from secure env
|
||||
|
||||
# Update stack after modifying YAML
|
||||
./update-stack.sh open-webui.yml
|
||||
|
||||
# Check result
|
||||
echo $? # 0 = success, 1 = failure
|
||||
```
|
||||
|
||||
### Remote Portainer Instance
|
||||
|
||||
```bash
|
||||
export PORTAINER_URL="https://portainer.example.com"
|
||||
./update-stack.sh my-stack.yml
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Storage
|
||||
|
||||
- Token stored in `.portainer-token` (gitignored)
|
||||
- File permissions: `600` (owner read/write only)
|
||||
- Token expires based on Portainer settings (default: 8 hours)
|
||||
- Re-authentication automatic if token expires
|
||||
|
||||
### Credentials
|
||||
|
||||
**DO NOT:**
|
||||
- ❌ Commit `.portainer-token` to git
|
||||
- ❌ Hardcode passwords in scripts
|
||||
- ❌ Share tokens between users
|
||||
- ❌ Use root/admin account for automation (create dedicated API user)
|
||||
|
||||
**DO:**
|
||||
- ✅ Use environment variables for non-interactive mode
|
||||
- ✅ Store credentials in secure password manager
|
||||
- ✅ Create dedicated Portainer user for automation
|
||||
- ✅ Rotate passwords regularly
|
||||
- ✅ Use `.gitignore` to exclude token file
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Create Automation User:**
|
||||
```
|
||||
Portainer → Users → Add User
|
||||
Username: portainer-automation
|
||||
Role: Environment Administrator (or custom)
|
||||
```
|
||||
|
||||
2. **Use Environment Variables:**
|
||||
```bash
|
||||
# In ~/.bashrc or secure environment
|
||||
export PORTAINER_USERNAME="portainer-automation"
|
||||
export PORTAINER_PASSWORD="$(pass show portainer/automation)" # from password manager
|
||||
```
|
||||
|
||||
3. **Restrict Permissions:**
|
||||
- Grant minimum required permissions
|
||||
- Limit to specific environments/stacks if possible
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Failed
|
||||
|
||||
```
|
||||
[ERROR] Failed to authenticate. Check credentials and try again.
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
- Verify username/password are correct
|
||||
- Check Portainer is accessible: `curl http://localhost:8080/api/status`
|
||||
- Ensure user has admin/environment admin role
|
||||
- Try removing `.portainer-token` and re-authenticating
|
||||
|
||||
### Stack Not Found
|
||||
|
||||
```
|
||||
[ERROR] Stack 'my-stack' not found in Portainer
|
||||
Available stacks:
|
||||
- open-webui
|
||||
- ollama
|
||||
- core-api
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
- Verify stack name matches filename (without .yml)
|
||||
- Check stack exists in Portainer UI
|
||||
- Stack name is case-sensitive
|
||||
- Create stack in Portainer first if it doesn't exist
|
||||
|
||||
### Connection Refused
|
||||
|
||||
```
|
||||
[ERROR] Failed to connect to Portainer at http://localhost:8080
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
- Check Portainer is running: `docker ps | grep portainer`
|
||||
- Verify port: Portainer default is 8080
|
||||
- Set `PORTAINER_URL` if using different port/host
|
||||
- Check firewall rules if accessing remote instance
|
||||
|
||||
### Token Expired
|
||||
|
||||
```
|
||||
[ERROR] Invalid authentication token
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
- Delete token file: `rm .portainer-token`
|
||||
- Re-run script to re-authenticate
|
||||
- Check Portainer token expiration settings
|
||||
|
||||
### YAML Validation Error
|
||||
|
||||
```
|
||||
[ERROR] Stack update failed: invalid compose file
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
- Validate YAML syntax: `yamllint open-webui.yml`
|
||||
- Check Docker Compose version compatibility
|
||||
- Review Portainer logs: `docker logs portainer`
|
||||
- Test with `docker compose config -f open-webui.yml`
|
||||
|
||||
## Integration with LLM Agents
|
||||
|
||||
### Claude Code Workflow
|
||||
|
||||
This script is designed to integrate seamlessly with Claude Code workflows:
|
||||
|
||||
1. **Agent modifies YAML file:**
|
||||
```python
|
||||
# Claude uses Edit tool to update open-webui.yml
|
||||
```
|
||||
|
||||
2. **Agent calls update script:**
|
||||
```bash
|
||||
cd /home/jpmschweitzer/Projects/portainer-core/stacks
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
3. **Agent verifies deployment:**
|
||||
```bash
|
||||
docker logs open-webui --tail 20
|
||||
curl http://localhost:82 # Verify service
|
||||
```
|
||||
|
||||
### Setting Up for Claude
|
||||
|
||||
Add to user profile or environment:
|
||||
|
||||
```bash
|
||||
# In ~/.bashrc or secure location
|
||||
export PORTAINER_USERNAME="admin"
|
||||
export PORTAINER_PASSWORD="your-secure-password"
|
||||
|
||||
# Or use password manager
|
||||
export PORTAINER_PASSWORD="$(pass show portainer/admin)"
|
||||
```
|
||||
|
||||
Then Claude can directly call:
|
||||
```bash
|
||||
./update-stack.sh <stack-file.yml>
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Portainer URL
|
||||
|
||||
```bash
|
||||
# Connect to remote Portainer
|
||||
export PORTAINER_URL="https://portainer.mydomain.com"
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
### Token Management
|
||||
|
||||
```bash
|
||||
# View current token (for debugging)
|
||||
cat .portainer-token | base64 -d | jq
|
||||
|
||||
# Force re-authentication
|
||||
rm .portainer-token
|
||||
./update-stack.sh open-webui.yml
|
||||
|
||||
# Use specific token
|
||||
echo "your-jwt-token-here" > .portainer-token
|
||||
chmod 600 .portainer-token
|
||||
```
|
||||
|
||||
### Dry Run (Check Only)
|
||||
|
||||
```bash
|
||||
# Validate YAML before updating
|
||||
docker compose -f open-webui.yml config
|
||||
|
||||
# Check current stack status
|
||||
curl -s http://localhost:8080/api/stacks \
|
||||
-H "Authorization: Bearer $(cat .portainer-token)" | jq
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
The script uses these Portainer API endpoints:
|
||||
|
||||
- `POST /api/auth` - Authenticate and get token
|
||||
- `GET /api/endpoints` - List Docker endpoints
|
||||
- `GET /api/stacks` - List all stacks
|
||||
- `PUT /api/stacks/{id}` - Update specific stack
|
||||
|
||||
For full API documentation: https://docs.portainer.io/api/docs
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Tasks
|
||||
|
||||
- **Monthly:** Rotate automation user password
|
||||
- **Quarterly:** Review and audit API access logs
|
||||
- **After incidents:** Revoke and regenerate tokens
|
||||
|
||||
### Token Rotation
|
||||
|
||||
```bash
|
||||
# Revoke old token (Portainer UI)
|
||||
Portainer → Users → [user] → Access Tokens → Revoke All
|
||||
|
||||
# Re-authenticate
|
||||
rm .portainer-token
|
||||
./update-stack.sh open-webui.yml
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check Portainer logs: `docker logs portainer`
|
||||
2. Review this guide's Troubleshooting section
|
||||
3. Check Portainer API docs: https://docs.portainer.io/api/docs
|
||||
4. Open issue in project repository
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-11-14*
|
||||
Reference in New Issue
Block a user