Compare commits
15 Commits
344baa16aa
...
b9acac33e2
| Author | SHA1 | Date |
|---|---|---|
|
|
b9acac33e2 | |
|
|
773337bd88 | |
|
|
cf793d7e7f | |
|
|
2cddb4ab04 | |
|
|
e36b8fa7e1 | |
|
|
1c9313c7b0 | |
|
|
dccaca5720 | |
|
|
9650772767 | |
|
|
1020c4559a | |
|
|
5e04a2fb0c | |
|
|
a114a5002e | |
|
|
116e55545d | |
|
|
4d85473223 | |
|
|
7067cb8c89 | |
|
|
49b2a6c38a |
|
|
@ -5,6 +5,9 @@
|
||||||
*.tfstate
|
*.tfstate
|
||||||
*.tfstate.*
|
*.tfstate.*
|
||||||
|
|
||||||
|
# Terraform lockfiles
|
||||||
|
*.terraform.lock.hcl
|
||||||
|
|
||||||
# Crash log files
|
# Crash log files
|
||||||
crash.log
|
crash.log
|
||||||
crash.*.log
|
crash.*.log
|
||||||
|
|
@ -36,4 +39,6 @@ override.tf.json
|
||||||
.terraformrc
|
.terraformrc
|
||||||
terraform.rc
|
terraform.rc
|
||||||
|
|
||||||
|
# Local scratch / retired assets, not meant for version control
|
||||||
|
junkyard/
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
customModes:
|
||||||
|
- slug: service-deployment
|
||||||
|
name: "🚀 Service Deployment"
|
||||||
|
description: Deploy new services to the mars ship infrastructure using quadlets/modules
|
||||||
|
roleDefinition: |
|
||||||
|
You are an expert DevOps engineer tasked with deploying new services to the mars ship infrastructure. Your goal is to create new quadlet modules for services and add them to the mars ship configuration. You specialize in adapting Docker Compose deployments to the quadlet-app module structure, integrating with existing services, and following established infrastructure patterns.
|
||||||
|
groups:
|
||||||
|
- read
|
||||||
|
- edit
|
||||||
|
- command
|
||||||
|
- browser
|
||||||
|
- mcp
|
||||||
|
whenToUse: |
|
||||||
|
When you want to deploy a new service to the mars ship infrastructure and need to:
|
||||||
|
- Create a new quadlet module for the service
|
||||||
|
- Adapt Docker Compose configurations to the quadlet-app structure
|
||||||
|
- Integrate with existing services (postgres, valkey, minio, rabbitmq)
|
||||||
|
- Add the service to the mars ship configuration
|
||||||
|
customInstructions: |
|
||||||
|
**Initial Question:**
|
||||||
|
Always start by asking: "Please provide the OCI container image/tag that you want to deploy to the mars ship."
|
||||||
|
|
||||||
|
**Deployment Process:**
|
||||||
|
|
||||||
|
1. **Research the Service Deployment:**
|
||||||
|
- Research how the service is typically deployed using Docker Compose
|
||||||
|
- Look for official documentation and Docker Compose examples
|
||||||
|
- Identify required environment variables, ports, volumes, and health checks
|
||||||
|
- Determine dependencies on other services
|
||||||
|
|
||||||
|
2. **Analyze Existing Mars Ship Services:**
|
||||||
|
- Review available services for dependencies:
|
||||||
|
- **postgres**: Database service (port 5432)
|
||||||
|
- **valkey**: Redis-compatible key-value store (port 6379)
|
||||||
|
- **minio**: S3-compatible object storage (ports 9000, 9001)
|
||||||
|
- **rabbitmq**: Message queue (port 5672)
|
||||||
|
- **oci-proxy**: Docker socket proxy (port 2375)
|
||||||
|
- Understand existing environment variable patterns
|
||||||
|
- Follow established port allocation and volume mounting patterns
|
||||||
|
|
||||||
|
3. **Create the Quadlet Module:**
|
||||||
|
- Create directory: `quadlets/modules/[service-name]/main.tf`
|
||||||
|
- Use the quadlet-app base module with this structure:
|
||||||
|
```hcl
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "[service-name]" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "[service-name]"
|
||||||
|
image = "[oci-image-from-user]"
|
||||||
|
ports = ["port:port"]
|
||||||
|
volumes = ["/host/path:/container/path:Z"]
|
||||||
|
environment = {
|
||||||
|
ENV_VAR = "value"
|
||||||
|
}
|
||||||
|
command = ["command", "args"]
|
||||||
|
healthcmd = "health-check-command"
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "service-name"
|
||||||
|
domain = "service-name.${var.server_domain}"
|
||||||
|
port = "port"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.[service-name].app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.[service-name].installed]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Configure Service Integration:**
|
||||||
|
- **Database Integration**: Use postgres connection: `postgres:5432`
|
||||||
|
- **Redis/Valkey Integration**: Use valkey connection: `valkey:6379`
|
||||||
|
- **MinIO/S3 Integration**: Use minio connection: `minio:9000`
|
||||||
|
- **RabbitMQ Integration**: Use rabbitmq connection: `rabbitmq:5672`
|
||||||
|
|
||||||
|
5. **Add to Mars Ship Configuration:**
|
||||||
|
- Add the module to `ships/mars/main.tf`:
|
||||||
|
```hcl
|
||||||
|
module "[service-name]" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/[service-name]"
|
||||||
|
server_ip = var.server
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Place appropriately based on dependencies (use `module.oci-proxy.installed` if needed)
|
||||||
|
|
||||||
|
6. **Handle Special Configurations:**
|
||||||
|
- **For passwords**: Use `random_password` resource like postgres/minio modules
|
||||||
|
- **For multiple ports**: Configure all ports and create separate HAProxy entries
|
||||||
|
- **For volumes**: Use pattern `/opt/storage/data/[service-name]:/container/path:Z`
|
||||||
|
|
||||||
|
7. **Ask Follow-up Questions When:**
|
||||||
|
- Service requires multiple containers (infrastructure only supports single containers)
|
||||||
|
- Service needs unusual configurations not seen in existing modules
|
||||||
|
- Dependencies on existing services are unclear
|
||||||
|
- Special security configurations are required
|
||||||
|
|
||||||
|
8. **Final Implementation:**
|
||||||
|
- Create the complete module file
|
||||||
|
- Add the module to mars ship configuration
|
||||||
|
- Provide exact files that need to be created/modified
|
||||||
|
- Include necessary deployment commands
|
||||||
|
|
||||||
|
**Key Points to Remember:**
|
||||||
|
- All services use the `quadlet-app` base module
|
||||||
|
- Services are deployed as single containers (no multi-container docker-compose)
|
||||||
|
- Use existing services for dependencies
|
||||||
|
- Follow established patterns for ports, volumes, and environment variables
|
||||||
|
- Always include HAProxy configuration for web services
|
||||||
|
- Use the mars ship domain for service URLs
|
||||||
|
|
@ -49,17 +49,29 @@ module "valkey" {
|
||||||
ssh_private_key_path = var.ssh_private_key_path
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
}
|
}
|
||||||
|
|
||||||
# module "vw-hub" {
|
module "airsonic-advanced" {
|
||||||
# wait_on = module.minio.installed
|
wait_on = module.hetzner.installed
|
||||||
#
|
source = "./modules/airsonic-advanced"
|
||||||
# source = "./modules/vw-hub"
|
server_ip = module.hetzner.server_ip
|
||||||
# server_ip = module.hetzner.server_ip
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
# ssh_private_key_path = var.ssh_private_key_path
|
server_domain = module.hetzner.server_domain
|
||||||
# domain = "hub.${module.hetzner.server_domain}"
|
}
|
||||||
# s3_access_key = module.minio.access_key
|
|
||||||
# s3_secret_key = module.minio.secret_key
|
module "mopidy" {
|
||||||
# s3_server = module.minio.server
|
wait_on = module.hetzner.installed
|
||||||
# }
|
source = "./modules/mopidy"
|
||||||
|
server_ip = module.hetzner.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
server_domain = module.hetzner.server_domain
|
||||||
|
}
|
||||||
|
|
||||||
|
module "beets" {
|
||||||
|
wait_on = module.hetzner.installed
|
||||||
|
source = "./modules/beets"
|
||||||
|
server_ip = module.hetzner.server_ip
|
||||||
|
server_domain = module.hetzner.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
output "minio_app_urls" {
|
output "minio_app_urls" {
|
||||||
value = module.minio.app_urls
|
value = module.minio.app_urls
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "psql_db" {
|
||||||
|
source = "../postgres/tenant"
|
||||||
|
|
||||||
|
tenant_name = "affine"
|
||||||
|
admin_username = "postgres"
|
||||||
|
admin_password = var.postgres_password
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "affine" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
restart_policy = "never"
|
||||||
|
|
||||||
|
app_name = "affine"
|
||||||
|
image = "ghcr.io/toeverything/affine:stable"
|
||||||
|
ports = ["3020:3010"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/affine/storage:/root/.affine/storage:Z",
|
||||||
|
"/opt/storage/data/affine/config:/root/.affine/config:Z"
|
||||||
|
]
|
||||||
|
environment = {
|
||||||
|
DATABASE_URL = "postgresql://affine:${module.psql_db.password}@systemd-postgres:5432/affine"
|
||||||
|
REDIS_SERVER_HOST = "systemd-valkey"
|
||||||
|
AFFINE_INDEXER_ENABLED = "false"
|
||||||
|
}
|
||||||
|
command = [
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
"\"node ./scripts/self-host-predeploy.js && node ./dist/main.js\""
|
||||||
|
]
|
||||||
|
healthcmd = "curl -f http://localhost:3010 || exit 1"
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "affine"
|
||||||
|
domain = "affine.${var.server_domain}"
|
||||||
|
port = "3020"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
depends_on_services = ["postgres.service", "valkey.service"]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.affine.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.affine.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "airsonic-advanced" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "airsonic-advanced"
|
||||||
|
image = "lscr.io/linuxserver/airsonic-advanced:latest"
|
||||||
|
ports = ["4040:4040"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/airsonic-advanced:/config:Z",
|
||||||
|
"/opt/storage/media/music:/music:Z",
|
||||||
|
]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
PUID="1001"
|
||||||
|
PGID="1001"
|
||||||
|
TZ="Etc/UTC"
|
||||||
|
#JAVA_OPTS="-Dserver.address=127.0.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "airsonic-advanced"
|
||||||
|
domain = "airsonic.${var.server_domain}"
|
||||||
|
port = "4040"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.airsonic-advanced.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.airsonic-advanced.installed]
|
||||||
|
}
|
||||||
|
|
@ -12,25 +12,57 @@ variable "ssh_private_key_path" {
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
module "redis" {
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "encryption_key" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "jwt_secret" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
module "arcane" {
|
||||||
source = "../quadlet-app"
|
source = "../quadlet-app"
|
||||||
wait_on = var.wait_on
|
wait_on = var.wait_on
|
||||||
|
|
||||||
server_ip = var.server_ip
|
server_ip = var.server_ip
|
||||||
ssh_private_key_path = var.ssh_private_key_path
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
app_name = "redis"
|
app_name = "arcane"
|
||||||
image = "docker.io/redis:7-alpine"
|
image = "ghcr.io/ofkm/arcane:latest"
|
||||||
ports = ["6379:6379"]
|
ports = ["3552:3552"]
|
||||||
volumes = ["/opt/storage/data/redis:/data:Z"]
|
volumes = ["/opt/storage/data/arcane:/app/data:Z", "/run/user/1001/podman/podman.sock:/var/run/docker.sock:Z"]
|
||||||
command = ["redis-server", "--appendonly", "yes"]
|
#volumes = ["/opt/storage/data/arcane:/app/data:Z"]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
#DOCKER_HOST = "tcp://localhost:2375"
|
||||||
|
APP_URL = "https://${var.server_domain}"
|
||||||
|
ENCRYPTION_KEY = random_password.encryption_key.result
|
||||||
|
JWT_SECRET = random_password.jwt_secret.result
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "arcane"
|
||||||
|
domain = "${var.server_domain}"
|
||||||
|
port = "3552"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output "app_urls" {
|
output "app_urls" {
|
||||||
value = module.redis.app_urls
|
value = module.arcane.app_urls
|
||||||
}
|
}
|
||||||
|
|
||||||
output "installed" {
|
output "installed" {
|
||||||
value = true
|
value = true
|
||||||
depends_on = [module.redis.installed]
|
depends_on = [module.arcane.installed]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "beets" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "beets"
|
||||||
|
image = "lscr.io/linuxserver/beets:latest"
|
||||||
|
ports = ["8337:8337"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/beets/config:/config:Z",
|
||||||
|
"/opt/storage/media/music:/music:z",
|
||||||
|
"/opt/storage/media/downloads:/downloads:z"
|
||||||
|
]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
PUID = "1001"
|
||||||
|
PGID = "1001"
|
||||||
|
TZ = "Europe/Amsterdam"
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "beets"
|
||||||
|
domain = "beets.${var.server_domain}"
|
||||||
|
port = "8337"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.beets.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.beets.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "deeptutor" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "deeptutor"
|
||||||
|
image = "ghcr.io/hkuds/deeptutor:latest"
|
||||||
|
ports = ["8001:8001", "3782:3782"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/deeptutor/config:/app/config:Z",
|
||||||
|
"/opt/storage/data/deeptutor/user:/app/data/user:Z",
|
||||||
|
"/opt/storage/data/deeptutor/knowledge_bases:/app/data/knowledge_bases:Z",
|
||||||
|
]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
# LLM Configuration (Required - set these in terraform.tfvars)
|
||||||
|
LLM_BINDING = "openai"
|
||||||
|
LLM_MODEL = "deepseek/deepseek-v3.2"
|
||||||
|
LLM_BINDING_API_KEY = "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"
|
||||||
|
LLM_BINDING_HOST = "https://openrouter.ai/api/v1"
|
||||||
|
DISABLE_SSL_VERIFY = "false"
|
||||||
|
|
||||||
|
# Embedding Configuration (Required for Knowledge Base)
|
||||||
|
EMBEDDING_BINDING = "openai"
|
||||||
|
EMBEDDING_MODEL = "text-embedding-3-large"
|
||||||
|
EMBEDDING_BINDING_API_KEY = "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"
|
||||||
|
EMBEDDING_BINDING_HOST = "https://openrouter.ai/api/v1"
|
||||||
|
EMBEDDING_DIM = "3072"
|
||||||
|
EMBEDDING_MAX_TOKENS = "8192"
|
||||||
|
|
||||||
|
# TTS Configuration (Optional)
|
||||||
|
TTS_MODEL = ""
|
||||||
|
TTS_API_KEY = ""
|
||||||
|
TTS_URL = ""
|
||||||
|
TTS_VOICE = "alloy"
|
||||||
|
|
||||||
|
# Web Search Configuration (Optional)
|
||||||
|
PERPLEXITY_API_KEY = ""
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
RAG_TOOL_MODULE_LOG_LEVEL = "INFO"
|
||||||
|
|
||||||
|
# Service Ports
|
||||||
|
BACKEND_PORT = "8001"
|
||||||
|
FRONTEND_PORT = "3782"
|
||||||
|
|
||||||
|
# External API URL (for browser access if different from localhost)
|
||||||
|
NEXT_PUBLIC_API_BASE_EXTERNAL = "https://deeptutor-api.${var.server_domain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
healthcmd = "curl -f http://localhost:8001/ || exit 1"
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "deeptutor-frontend"
|
||||||
|
domain = "deeptutor.${var.server_domain}"
|
||||||
|
port = "3782"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "deeptutor-api"
|
||||||
|
domain = "deeptutor-api.${var.server_domain}"
|
||||||
|
port = "8001"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.deeptutor.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.deeptutor.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "username" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "password" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
module "redis" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "documentdb"
|
||||||
|
image = "ghcr.io/documentdb/documentdb/documentdb-local:latest"
|
||||||
|
ports = ["27017:27017"]
|
||||||
|
volumes = ["/opt/storage/data/documentdb:/data:Z"]
|
||||||
|
environment = {
|
||||||
|
USERNAME = random_password.username.result
|
||||||
|
PASSWORD = random_password.password.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.redis.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.redis.installed]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "username" {
|
||||||
|
value = random_password.username.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "password" {
|
||||||
|
value = random_password.password.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "postgres_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "forgejo_secret_key" {
|
||||||
|
length = 64
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "forgejo_internal_token" {
|
||||||
|
length = 64
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "forgejo_jwt_secret" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
module "psql_db" {
|
||||||
|
source = "../postgres/tenant"
|
||||||
|
|
||||||
|
tenant_name = "forgejo"
|
||||||
|
admin_username = "postgres"
|
||||||
|
admin_password = var.postgres_password
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "forgejo" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "forgejo"
|
||||||
|
image = "codeberg.org/forgejo/forgejo:13.0"
|
||||||
|
ports = ["3000:3000", "2222:22"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/forgejo:/data:Z",
|
||||||
|
"/etc/localtime:/etc/localtime:ro"
|
||||||
|
]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
USER_UID = "1000"
|
||||||
|
USER_GID = "1000"
|
||||||
|
FORGEJO__database__DB_TYPE = "postgres"
|
||||||
|
FORGEJO__database__HOST = "systemd-postgres:5432"
|
||||||
|
FORGEJO__database__NAME = "forgejo"
|
||||||
|
FORGEJO__database__USER = module.psql_db.username
|
||||||
|
FORGEJO__database__PASSWD = module.psql_db.password
|
||||||
|
FORGEJO__server__DOMAIN = "forgejo.${var.server_domain}"
|
||||||
|
FORGEJO__server__ROOT_URL = "https://forgejo.${var.server_domain}/"
|
||||||
|
FORGEJO__server__SSH_DOMAIN = "forgejo.${var.server_domain}"
|
||||||
|
FORGEJO__server__SSH_PORT = "2222"
|
||||||
|
FORGEJO__security__INSTALL_LOCK = "true"
|
||||||
|
FORGEJO__security__SECRET_KEY = random_password.forgejo_secret_key.result
|
||||||
|
FORGEJO__security__INTERNAL_TOKEN = random_password.forgejo_internal_token.result
|
||||||
|
FORGEJO__oauth2__JWT_SECRET = random_password.forgejo_jwt_secret.result
|
||||||
|
}
|
||||||
|
|
||||||
|
healthcmd = "curl -f http://localhost:3000/api/v1/version || exit 1"
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "forgejo"
|
||||||
|
domain = "forgejo.${var.server_domain}"
|
||||||
|
port = "3000"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.forgejo.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.forgejo.installed]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "secret_key" {
|
||||||
|
value = random_password.forgejo_secret_key.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "internal_token" {
|
||||||
|
value = random_password.forgejo_internal_token.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "jwt_secret" {
|
||||||
|
value = random_password.forgejo_jwt_secret.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "gonic" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "gonic"
|
||||||
|
image = "docker.io/sentriz/gonic:latest"
|
||||||
|
ports = ["4747:80"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/gonic:/config:Z",
|
||||||
|
"/opt/storage/media/music:/music:Z",
|
||||||
|
"/opt/storage/media/podcasts:/podcasts:Z",
|
||||||
|
"/opt/storage/media/playlists:/playlists:Z",
|
||||||
|
"/opt/storage/cache/gonic:/cache:Z",
|
||||||
|
]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
TZ="Etc/UTC"
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "gonic"
|
||||||
|
domain = "gonic.${var.server_domain}"
|
||||||
|
port = "4747"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.gonic.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.gonic.installed]
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,11 @@ variable "server_domain" {
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "https" {
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
resource "random_password" "minio_access_key" {
|
resource "random_password" "minio_access_key" {
|
||||||
length = 20
|
length = 20
|
||||||
special = false
|
special = false
|
||||||
|
|
@ -45,7 +50,7 @@ module "minio" {
|
||||||
MINIO_ROOT_USER = random_password.minio_access_key.result
|
MINIO_ROOT_USER = random_password.minio_access_key.result
|
||||||
MINIO_ROOT_PASSWORD = random_password.minio_secret_key.result
|
MINIO_ROOT_PASSWORD = random_password.minio_secret_key.result
|
||||||
MINIO_CONSOLE_ADDRESS = ":9001"
|
MINIO_CONSOLE_ADDRESS = ":9001"
|
||||||
MINIO_BROWSER_REDIRECT_URL = "http://storage.${var.server_domain}"
|
MINIO_BROWSER_REDIRECT_URL = "${var.https ? "https" : "http" }://storage.${var.server_domain}"
|
||||||
}
|
}
|
||||||
|
|
||||||
command = ["server", "/data", "--console-address", ":9001"]
|
command = ["server", "/data", "--console-address", ":9001"]
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,7 @@ resource "minio_iam_user_policy_attachment" "overlay" {
|
||||||
resource "minio_iam_service_account" "overlay" {
|
resource "minio_iam_service_account" "overlay" {
|
||||||
depends_on = [minio_iam_user.overlay, minio_s3_bucket.overlay, minio_s3_bucket.uploads]
|
depends_on = [minio_iam_user.overlay, minio_s3_bucket.overlay, minio_s3_bucket.uploads]
|
||||||
target_user = minio_iam_user.overlay.name
|
target_user = minio_iam_user.overlay.name
|
||||||
|
name = var.name
|
||||||
policy = jsonencode({
|
policy = jsonencode({
|
||||||
Version = "2012-10-17"
|
Version = "2012-10-17"
|
||||||
Statement = [
|
Statement = [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "mopidy" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "mopidy"
|
||||||
|
image = "docker.io/ivdata/mopidy"
|
||||||
|
ports = ["6600:6600", "6680:6680"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/mopidy:/var/lib/mopidy:Z",
|
||||||
|
"/opt/storage/data/mopidy/mopidy.conf:/home/mopidy/.config/mopidy/mopidy.conf:Z",
|
||||||
|
"/opt/storage/media/music:/media/music:ro,Z",
|
||||||
|
]
|
||||||
|
|
||||||
|
# environment = {
|
||||||
|
# SPOTIFY_CLIENT_ID="ff4a141c-cb52-4569-9848-c0468da66bd1"
|
||||||
|
# SPOTIFY_CLIENT_SECRET="VVXVtoiY8DanFLl9BnpHvLygXCchboHoEewoALTHIaA="
|
||||||
|
# }
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "mopidy"
|
||||||
|
domain = "mopidy.${var.server_domain}"
|
||||||
|
port = "6680"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.mopidy.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.mopidy.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "navidrome" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "navidrome"
|
||||||
|
image = "docker.io/deluan/navidrome:latest"
|
||||||
|
ports = ["4533:4533"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/navidrome:/data:Z",
|
||||||
|
"/opt/storage/media/music:/music:ro,Z",
|
||||||
|
]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
ND_LOGLEVEL="info"
|
||||||
|
ND_ENABLEINSIGHTSCOLLECTOR="false"
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "navidrome"
|
||||||
|
domain = "navidrome.${var.server_domain}"
|
||||||
|
port = "4533"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.navidrome.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.navidrome.installed]
|
||||||
|
}
|
||||||
|
|
@ -12,25 +12,51 @@ variable "ssh_private_key_path" {
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
module "redis" {
|
module "oci-proxy" {
|
||||||
source = "../quadlet-app"
|
source = "../quadlet-app"
|
||||||
wait_on = var.wait_on
|
wait_on = var.wait_on
|
||||||
|
|
||||||
server_ip = var.server_ip
|
server_ip = var.server_ip
|
||||||
ssh_private_key_path = var.ssh_private_key_path
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
app_name = "redis"
|
app_name = "oci-proxy"
|
||||||
image = "docker.io/redis:7-alpine"
|
image = "docker.io/tecnativa/docker-socket-proxy"
|
||||||
ports = ["6379:6379"]
|
ports = ["2375:2375"]
|
||||||
volumes = ["/opt/storage/data/redis:/data:Z"]
|
volumes = ["/run/user/1001/podman/podman.sock:/var/run/docker.sock:Z"]
|
||||||
command = ["redis-server", "--appendonly", "yes"]
|
|
||||||
|
environment = {
|
||||||
|
LOG_LEVEL = "info"
|
||||||
|
EVENTS = 1
|
||||||
|
PING = 1
|
||||||
|
VERSION = 1
|
||||||
|
AUTH = 0
|
||||||
|
SECRETS = 0
|
||||||
|
POST = 1
|
||||||
|
BUILD = 0
|
||||||
|
COMMIT = 0
|
||||||
|
CONFIGS = 0
|
||||||
|
CONTAINERS = 1
|
||||||
|
DISTRIBUTION = 0
|
||||||
|
EXEC = 0
|
||||||
|
IMAGES = 1
|
||||||
|
INFO = 1
|
||||||
|
NETWORKS = 1
|
||||||
|
NODES = 0
|
||||||
|
PLUGINS = 0
|
||||||
|
SERVICES = 1
|
||||||
|
SESSION = 0
|
||||||
|
SWARM = 0
|
||||||
|
SYSTEM = 0
|
||||||
|
TASKS = 1
|
||||||
|
VOLUMES = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output "app_urls" {
|
output "app_urls" {
|
||||||
value = module.redis.app_urls
|
value = module.oci-proxy.app_urls
|
||||||
}
|
}
|
||||||
|
|
||||||
output "installed" {
|
output "installed" {
|
||||||
value = true
|
value = true
|
||||||
depends_on = [module.redis.installed]
|
depends_on = [module.oci-proxy.installed]
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate random keys for esign
|
||||||
|
resource "random_password" "app_id" {
|
||||||
|
length = 12
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "master_key" {
|
||||||
|
length = 32
|
||||||
|
special = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "esign" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "esign"
|
||||||
|
image = "docker.io/esign/esignserver:main"
|
||||||
|
ports = ["8180:8080"]
|
||||||
|
volumes = ["/opt/storage/data/esign:/usr/src/app/files:Z"]
|
||||||
|
environment = {
|
||||||
|
# Node environment
|
||||||
|
NODE_ENV = "production"
|
||||||
|
|
||||||
|
# App identifiers
|
||||||
|
APP_ID = random_password.app_id.result
|
||||||
|
REACT_APP_APPID = random_password.app_id.result
|
||||||
|
appName = "Four Lights - eSign"
|
||||||
|
MASTER_KEY = random_password.master_key.result
|
||||||
|
|
||||||
|
# Frontend config
|
||||||
|
PUBLIC_URL = "https://esign.${var.server_domain}"
|
||||||
|
REACT_APP_SERVERURL = "https://esign.${var.server_domain}/api/app"
|
||||||
|
GENERATE_SOURCEMAP = "false"
|
||||||
|
|
||||||
|
# Backend API config
|
||||||
|
SERVER_URL = "https://esign.${var.server_domain}/api/app"
|
||||||
|
PARSE_MOUNT = "/app"
|
||||||
|
|
||||||
|
# MongoDB connection settings
|
||||||
|
MONGODB_URI = "mongodb://systemd-documentdb:27017/esign"
|
||||||
|
|
||||||
|
# MinIO/S3 storage configuration
|
||||||
|
DO_SPACE = "esign"
|
||||||
|
DO_ENDPOINT = "systemd-minio:9000"
|
||||||
|
DO_BASEURL = "https://esign.${var.server_domain}/minio"
|
||||||
|
DO_ACCESS_KEY_ID = "esign"
|
||||||
|
DO_SECRET_ACCESS_KEY = "esign"
|
||||||
|
DO_REGION = "us-east-1"
|
||||||
|
|
||||||
|
# Email config (commented out - configure as needed)
|
||||||
|
# SMTP_ENABLE = ""
|
||||||
|
# SMTP_HOST = ""
|
||||||
|
# SMTP_PORT = ""
|
||||||
|
# SMTP_USER_EMAIL = ""
|
||||||
|
# SMTP_PASS = ""
|
||||||
|
|
||||||
|
# Document signing (optional)
|
||||||
|
PFX_BASE64 = ""
|
||||||
|
PASS_PHRASE = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "esign"
|
||||||
|
domain = "esign.${var.server_domain}"
|
||||||
|
port = "8180"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.esign.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_id" {
|
||||||
|
value = random_password.app_id.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "master_key" {
|
||||||
|
value = random_password.master_key.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.esign.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "admin" {
|
||||||
|
source = "../../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "plane-admin"
|
||||||
|
image = "docker.io/makeplane/plane-admin:v1.0.0"
|
||||||
|
ports = ["3010:3000"]
|
||||||
|
environment = {
|
||||||
|
# API settings
|
||||||
|
NEXT_PUBLIC_API_BASE_URL = "https://plane.${var.server_domain}/api"
|
||||||
|
|
||||||
|
# Web settings
|
||||||
|
NEXT_PUBLIC_WEB_URL = "https://plane.${var.server_domain}/god-mode"
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
NODE_ENV = "production"
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "plane-admin"
|
||||||
|
domain = "plane.${var.server_domain}/god-mode/"
|
||||||
|
port = "3010"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.admin.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.admin.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
# Accept credentials from parent Plane module
|
||||||
|
variable "db_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "redis_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rabbitmq_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_access_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "api" {
|
||||||
|
source = "../../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
restart_policy = "never"
|
||||||
|
|
||||||
|
app_name = "plane-api"
|
||||||
|
image = "docker.io/makeplane/plane-backend:v1.0.0"
|
||||||
|
ports = ["3011:8000"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/plane-api:/var/www/plane:Z"
|
||||||
|
]
|
||||||
|
environment = {
|
||||||
|
# Database settings
|
||||||
|
DATABASE_URL = "postgresql://plane:${var.db_password}@systemd-postgres:5432/plane"
|
||||||
|
POSTGRES_USER = "plane"
|
||||||
|
POSTGRES_PASSWORD = var.db_password
|
||||||
|
POSTGRES_DB = "plane"
|
||||||
|
|
||||||
|
# Redis settings
|
||||||
|
REDIS_URL = "redis://:${var.redis_password}@systemd-valkey:6379/0"
|
||||||
|
REDIS_HOST = "systemd-valkey"
|
||||||
|
REDIS_PORT = "6379"
|
||||||
|
|
||||||
|
# RabbitMQ settings
|
||||||
|
RABBITMQ_URL = "amqp://plane:${var.rabbitmq_password}@systemd-rabbitmq:5672/plane"
|
||||||
|
RABBITMQ_HOST = "systemd-rabbitmq"
|
||||||
|
RABBITMQ_PORT = "5672"
|
||||||
|
RABBITMQ_USER = "plane"
|
||||||
|
RABBITMQ_PASSWORD = var.rabbitmq_password
|
||||||
|
RABBITMQ_VHOST = "plane"
|
||||||
|
|
||||||
|
# MinIO/S3 settings
|
||||||
|
AWS_S3_ENDPOINT_URL = "http://systemd-minio:9000"
|
||||||
|
AWS_ACCESS_KEY_ID = var.minio_access_key
|
||||||
|
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
|
||||||
|
AWS_S3_BUCKET_NAME = "plane"
|
||||||
|
AWS_REGION = "us-east-1"
|
||||||
|
USE_MINIO = "1"
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
SECRET_KEY = var.secret_key
|
||||||
|
DEBUG = "0"
|
||||||
|
ENVIRONMENT = "production"
|
||||||
|
|
||||||
|
# Web settings
|
||||||
|
WEB_URL = "https://plane.${var.server_domain}/api/"
|
||||||
|
ADMIN_BASE_URL = "https://plane.${var.server_domain}/god-mode/"
|
||||||
|
SPACE_BASE_URL = "https://plane.${var.server_domain}/spaces/"
|
||||||
|
APP_BASE_URL = "https://plane.${var.server_domain}/"
|
||||||
|
LIVE_BASE_URL = "https://plane.${var.server_domain}/live/"
|
||||||
|
|
||||||
|
# File upload settings
|
||||||
|
FILE_SIZE_LIMIT = "5242880"
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
API_KEY_RATE_LIMIT = "60/minute"
|
||||||
|
|
||||||
|
# Workers
|
||||||
|
GUNICORN_WORKERS=3
|
||||||
|
|
||||||
|
# Email settings (optional - can be configured later)
|
||||||
|
# EMAIL_HOST = ""
|
||||||
|
# EMAIL_PORT = ""
|
||||||
|
# EMAIL_HOST_USER = ""
|
||||||
|
# EMAIL_HOST_PASSWORD = ""
|
||||||
|
# EMAIL_USE_TLS = "1"
|
||||||
|
|
||||||
|
# OpenAI/GPT settings (optional)
|
||||||
|
# OPENAI_API_KEY = ""
|
||||||
|
# GPT_ENGINE = "gpt-3.5-turbo"
|
||||||
|
}
|
||||||
|
command = ["./bin/docker-entrypoint-api.sh"]
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "plane-api"
|
||||||
|
domain = "plane.${var.server_domain}/api/"
|
||||||
|
port = "3011"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "plane-auth"
|
||||||
|
domain = "plane.${var.server_domain}/auth/"
|
||||||
|
port = "3011"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.api.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.api.installed]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "db_password" {
|
||||||
|
value = var.db_password
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "redis_password" {
|
||||||
|
value = var.redis_password
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "rabbitmq_password" {
|
||||||
|
value = var.rabbitmq_password
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "minio_access_key" {
|
||||||
|
value = var.minio_access_key
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "minio_secret_key" {
|
||||||
|
value = var.minio_secret_key
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "secret_key" {
|
||||||
|
value = var.secret_key
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "redis_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rabbitmq_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_access_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "beat-worker" {
|
||||||
|
source = "../../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
restart_policy = "never"
|
||||||
|
|
||||||
|
app_name = "plane-beat-worker"
|
||||||
|
image = "docker.io/makeplane/plane-backend:v1.0.0"
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/plane-beat-worker:/var/www/plane:Z"
|
||||||
|
]
|
||||||
|
environment = {
|
||||||
|
# Database settings
|
||||||
|
DATABASE_URL = "postgresql://plane:${var.db_password}@systemd-postgres:5432/plane"
|
||||||
|
POSTGRES_USER = "plane"
|
||||||
|
POSTGRES_PASSWORD = var.db_password
|
||||||
|
POSTGRES_DB = "plane"
|
||||||
|
|
||||||
|
# Redis settings
|
||||||
|
REDIS_URL = "redis://:${var.redis_password}@systemd-valkey:6379/0"
|
||||||
|
REDIS_HOST = "systemd-valkey"
|
||||||
|
REDIS_PORT = "6379"
|
||||||
|
|
||||||
|
# RabbitMQ settings
|
||||||
|
RABBITMQ_URL = "amqp://plane:${var.rabbitmq_password}@systemd-rabbitmq:5672/plane"
|
||||||
|
RABBITMQ_HOST = "systemd-rabbitmq"
|
||||||
|
RABBITMQ_PORT = "5672"
|
||||||
|
RABBITMQ_USER = "plane"
|
||||||
|
RABBITMQ_PASSWORD = var.rabbitmq_password
|
||||||
|
RABBITMQ_VHOST = "plane"
|
||||||
|
|
||||||
|
# MinIO/S3 settings
|
||||||
|
AWS_S3_ENDPOINT_URL = "http://systemd-minio:9000"
|
||||||
|
AWS_ACCESS_KEY_ID = var.minio_access_key
|
||||||
|
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
|
||||||
|
AWS_S3_BUCKET_NAME = "plane-uploads"
|
||||||
|
AWS_REGION = "us-east-1"
|
||||||
|
USE_MINIO = "1"
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
SECRET_KEY = var.secret_key
|
||||||
|
DEBUG = "0"
|
||||||
|
ENVIRONMENT = "production"
|
||||||
|
|
||||||
|
# Web settings
|
||||||
|
WEB_URL = "https://plane.${var.server_domain}"
|
||||||
|
API_BASE_URL = "https://plane.${var.server_domain}/api"
|
||||||
|
|
||||||
|
# File upload settings
|
||||||
|
FILE_SIZE_LIMIT = "5242880"
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
API_KEY_RATE_LIMIT = "60/minute"
|
||||||
|
}
|
||||||
|
command = ["./bin/docker-entrypoint-beat.sh"]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.beat-worker.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "live" {
|
||||||
|
source = "../../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "plane-live"
|
||||||
|
image = "docker.io/makeplane/plane-live:v1.0.0"
|
||||||
|
ports = ["3012:3000"]
|
||||||
|
environment = {
|
||||||
|
# API settings
|
||||||
|
NEXT_PUBLIC_API_BASE_URL = "https://plane.${var.server_domain}/api/"
|
||||||
|
|
||||||
|
# Web settings
|
||||||
|
NEXT_PUBLIC_WEB_URL = "https://plane.${var.server_domain}/live/"
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
NODE_ENV = "production"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_policy = "never"
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "plane-live"
|
||||||
|
domain = "plane.${var.server_domain}/live/"
|
||||||
|
port = "3012"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.live.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.live.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
# Credentials from existing services
|
||||||
|
variable "postgres_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_server" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_access_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rabbitmq_username" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rabbitmq_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate Plane-specific credentials
|
||||||
|
resource "random_password" "redis_password" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "secret_key" {
|
||||||
|
length = 64
|
||||||
|
special = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "psql_db" {
|
||||||
|
source = "../postgres/tenant"
|
||||||
|
|
||||||
|
tenant_name = "plane"
|
||||||
|
admin_username = "postgres"
|
||||||
|
admin_password = var.postgres_password
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "aqmp_db" {
|
||||||
|
source = "../rabbitmq/tenant"
|
||||||
|
|
||||||
|
tenant_name = "plane"
|
||||||
|
admin_username = var.rabbitmq_username
|
||||||
|
admin_password = var.rabbitmq_password
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "minio" {
|
||||||
|
source = "../minio/tenant"
|
||||||
|
|
||||||
|
name = "plane"
|
||||||
|
server = var.minio_server
|
||||||
|
access_key = var.minio_access_key
|
||||||
|
secret_key = var.minio_secret_key
|
||||||
|
tls = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deploy the core API service first
|
||||||
|
module "api" {
|
||||||
|
source = "./api"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
server_ip = var.server_ip
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
# Use credentials from existing services
|
||||||
|
redis_password = "" #random_password.redis_password.result
|
||||||
|
db_password = module.psql_db.password
|
||||||
|
rabbitmq_password = module.aqmp_db.password
|
||||||
|
|
||||||
|
minio_access_key = module.minio.access_key
|
||||||
|
minio_secret_key = module.minio.secret_key
|
||||||
|
secret_key = random_password.secret_key.result
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run database migrations after API is ready
|
||||||
|
module "migrator" {
|
||||||
|
source = "./migrator"
|
||||||
|
wait_on = module.api.installed
|
||||||
|
server_ip = var.server_ip
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
# Use credentials from API module
|
||||||
|
db_password = module.api.db_password
|
||||||
|
redis_password = module.api.redis_password
|
||||||
|
rabbitmq_password = module.api.rabbitmq_password
|
||||||
|
minio_access_key = module.minio.access_key
|
||||||
|
minio_secret_key = module.minio.secret_key
|
||||||
|
secret_key = module.api.secret_key
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start background workers after migrations
|
||||||
|
module "worker" {
|
||||||
|
source = "./worker"
|
||||||
|
wait_on = module.migrator.installed
|
||||||
|
server_ip = var.server_ip
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
# Use credentials from API module
|
||||||
|
db_password = module.api.db_password
|
||||||
|
redis_password = module.api.redis_password
|
||||||
|
rabbitmq_password = module.api.rabbitmq_password
|
||||||
|
minio_access_key = module.minio.access_key
|
||||||
|
minio_secret_key = module.minio.secret_key
|
||||||
|
secret_key = module.api.secret_key
|
||||||
|
}
|
||||||
|
|
||||||
|
module "beat-worker" {
|
||||||
|
source = "./beat-worker"
|
||||||
|
wait_on = module.migrator.installed
|
||||||
|
server_ip = var.server_ip
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
# Use credentials from API module
|
||||||
|
db_password = module.api.db_password
|
||||||
|
redis_password = module.api.redis_password
|
||||||
|
rabbitmq_password = module.api.rabbitmq_password
|
||||||
|
minio_access_key = module.minio.access_key
|
||||||
|
minio_secret_key = module.minio.secret_key
|
||||||
|
secret_key = module.api.secret_key
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start web interfaces after API is ready
|
||||||
|
module "web" {
|
||||||
|
source = "./web"
|
||||||
|
wait_on = module.api.installed
|
||||||
|
server_ip = var.server_ip
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "admin" {
|
||||||
|
source = "./admin"
|
||||||
|
wait_on = module.api.installed
|
||||||
|
server_ip = var.server_ip
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "space" {
|
||||||
|
source = "./space"
|
||||||
|
wait_on = module.api.installed
|
||||||
|
server_ip = var.server_ip
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "live" {
|
||||||
|
source = "./live"
|
||||||
|
wait_on = module.api.installed
|
||||||
|
server_ip = var.server_ip
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
# Aggregate all app URLs
|
||||||
|
locals {
|
||||||
|
app_urls = {
|
||||||
|
web = module.web.app_urls
|
||||||
|
admin = module.admin.app_urls
|
||||||
|
space = module.space.app_urls
|
||||||
|
live = module.live.app_urls
|
||||||
|
api = module.api.app_urls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = local.app_urls
|
||||||
|
description = "All Plane application URLs"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [
|
||||||
|
module.api.installed,
|
||||||
|
module.migrator.installed,
|
||||||
|
module.worker.installed,
|
||||||
|
module.beat-worker.installed,
|
||||||
|
module.web.installed,
|
||||||
|
module.admin.installed,
|
||||||
|
module.space.installed,
|
||||||
|
module.live.installed
|
||||||
|
]
|
||||||
|
description = "Whether all Plane services are installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "credentials" {
|
||||||
|
value = {
|
||||||
|
db_password = module.api.db_password
|
||||||
|
redis_password = module.api.redis_password
|
||||||
|
minio_access_key = module.api.minio_access_key
|
||||||
|
minio_secret_key = module.api.minio_secret_key
|
||||||
|
rabbitmq_password = module.api.rabbitmq_password
|
||||||
|
secret_key = module.api.secret_key
|
||||||
|
}
|
||||||
|
sensitive = true
|
||||||
|
description = "All Plane service credentials"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "main_url" {
|
||||||
|
value = "https://plane.${var.server_domain}"
|
||||||
|
description = "Main Plane application URL"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "redis_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rabbitmq_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_access_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "migrator" {
|
||||||
|
source = "../../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
restart_policy = "never"
|
||||||
|
|
||||||
|
app_name = "plane-migrator"
|
||||||
|
image = "docker.io/makeplane/plane-backend:v1.0.0"
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/plane-migrator:/var/www/plane:Z"
|
||||||
|
]
|
||||||
|
environment = {
|
||||||
|
# Database settings
|
||||||
|
DATABASE_URL = "postgresql://plane:${var.db_password}@systemd-postgres:5432/plane"
|
||||||
|
POSTGRES_USER = "plane"
|
||||||
|
POSTGRES_PASSWORD = var.db_password
|
||||||
|
POSTGRES_DB = "plane"
|
||||||
|
|
||||||
|
# Redis settings
|
||||||
|
REDIS_URL = "redis://:${var.redis_password}@systemd-valkey:6379/0"
|
||||||
|
REDIS_HOST = "systemd-valkey"
|
||||||
|
REDIS_PORT = "6379"
|
||||||
|
|
||||||
|
# RabbitMQ settings
|
||||||
|
RABBITMQ_URL = "amqp://plane:${var.rabbitmq_password}@systemd-rabbitmq:5672/plane"
|
||||||
|
RABBITMQ_HOST = "systemd-rabbitmq"
|
||||||
|
RABBITMQ_PORT = "5672"
|
||||||
|
RABBITMQ_USER = "plane"
|
||||||
|
RABBITMQ_PASSWORD = var.rabbitmq_password
|
||||||
|
RABBITMQ_VHOST = "plane"
|
||||||
|
|
||||||
|
# MinIO/S3 settings
|
||||||
|
AWS_S3_ENDPOINT_URL = "http://systemd-minio:9000"
|
||||||
|
AWS_ACCESS_KEY_ID = var.minio_access_key
|
||||||
|
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
|
||||||
|
AWS_S3_BUCKET_NAME = "plane-uploads"
|
||||||
|
AWS_REGION = "us-east-1"
|
||||||
|
USE_MINIO = "1"
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
SECRET_KEY = var.secret_key
|
||||||
|
DEBUG = "0"
|
||||||
|
ENVIRONMENT = "production"
|
||||||
|
|
||||||
|
# Web settings
|
||||||
|
WEB_URL = "https://plane.${var.server_domain}"
|
||||||
|
API_BASE_URL = "https://plane.${var.server_domain}/api"
|
||||||
|
|
||||||
|
# File upload settings
|
||||||
|
FILE_SIZE_LIMIT = "5242880"
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
API_KEY_RATE_LIMIT = "60/minute"
|
||||||
|
}
|
||||||
|
command = ["./bin/docker-entrypoint-migrator.sh"]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.migrator.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "space" {
|
||||||
|
source = "../../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
restart_policy = "never"
|
||||||
|
|
||||||
|
app_name = "plane-space"
|
||||||
|
image = "docker.io/makeplane/plane-space:v1.0.0"
|
||||||
|
ports = ["3013:3000"]
|
||||||
|
environment = {
|
||||||
|
# API settings
|
||||||
|
NEXT_PUBLIC_API_BASE_URL = "https://plane.${var.server_domain}/api"
|
||||||
|
|
||||||
|
# Web settings
|
||||||
|
NEXT_PUBLIC_WEB_URL = "https://plane.${var.server_domain}/spaces"
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
NODE_ENV = "production"
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "plane-space"
|
||||||
|
domain = "plane.${var.server_domain}/spaces/"
|
||||||
|
port = "3013"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.space.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.space.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "web" {
|
||||||
|
source = "../../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
restart_policy = "never"
|
||||||
|
|
||||||
|
app_name = "plane-web"
|
||||||
|
image = "docker.io/makeplane/plane-frontend:v1.0.0"
|
||||||
|
ports = ["3014:3000"]
|
||||||
|
environment = {
|
||||||
|
# API settings
|
||||||
|
NEXT_PUBLIC_API_BASE_URL = "https://plane.${var.server_domain}/api"
|
||||||
|
|
||||||
|
# Web settings
|
||||||
|
NEXT_PUBLIC_WEB_BASE_URL = "https://plane.${var.server_domain}"
|
||||||
|
|
||||||
|
NEXT_PUBLIC_ADMIN_BASE_URL = "https://plane.${var.server_domain}/god-mode"
|
||||||
|
NEXT_PUBLIC_SPACE_BASE_URL = "https://plane.${var.server_domain}/spaces"
|
||||||
|
NEXT_PUBLIC_LIVE_BASE_URL = "https://plane.${var.server_domain}/live"
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
NODE_ENV = "production"
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "plane-web"
|
||||||
|
domain = "plane.${var.server_domain}"
|
||||||
|
port = "3014"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.web.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.web.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "db_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "redis_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rabbitmq_password" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_access_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "minio_secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "secret_key" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "worker" {
|
||||||
|
source = "../../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
restart_policy = "never"
|
||||||
|
|
||||||
|
app_name = "plane-worker"
|
||||||
|
image = "docker.io/makeplane/plane-backend:v1.0.0"
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/plane-worker:/var/www/plane:Z"
|
||||||
|
]
|
||||||
|
environment = {
|
||||||
|
# Database settings
|
||||||
|
DATABASE_URL = "postgresql://plane:${var.db_password}@systemd-postgres:5432/plane"
|
||||||
|
POSTGRES_USER = "plane"
|
||||||
|
POSTGRES_PASSWORD = var.db_password
|
||||||
|
POSTGRES_DB = "plane"
|
||||||
|
PGDATA = "/var/lib/postgresql/data"
|
||||||
|
|
||||||
|
# Redis settings
|
||||||
|
REDIS_URL = "redis://:${var.redis_password}@systemd-valkey:6379/0"
|
||||||
|
REDIS_HOST = "systemd-valkey"
|
||||||
|
REDIS_PORT = "6379"
|
||||||
|
|
||||||
|
# RabbitMQ settings
|
||||||
|
RABBITMQ_URL = "amqp://plane:${var.rabbitmq_password}@systemd-rabbitmq:5672/plane"
|
||||||
|
RABBITMQ_HOST = "systemd-rabbitmq"
|
||||||
|
RABBITMQ_PORT = "5672"
|
||||||
|
RABBITMQ_USER = "plane"
|
||||||
|
RABBITMQ_PASSWORD = var.rabbitmq_password
|
||||||
|
RABBITMQ_VHOST = "plane"
|
||||||
|
|
||||||
|
# MinIO/S3 settings
|
||||||
|
AWS_S3_ENDPOINT_URL = "http://systemd-minio:9000"
|
||||||
|
AWS_ACCESS_KEY_ID = var.minio_access_key
|
||||||
|
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
|
||||||
|
AWS_S3_BUCKET_NAME = "plane-uploads"
|
||||||
|
AWS_REGION = "us-east-1"
|
||||||
|
USE_MINIO = "1"
|
||||||
|
|
||||||
|
# Application settings
|
||||||
|
SECRET_KEY = var.secret_key
|
||||||
|
DEBUG = "0"
|
||||||
|
ENVIRONMENT = "production"
|
||||||
|
|
||||||
|
# Web settings
|
||||||
|
WEB_URL = "https://plane.${var.server_domain}"
|
||||||
|
API_BASE_URL = "https://plane.${var.server_domain}/api"
|
||||||
|
|
||||||
|
# File upload settings
|
||||||
|
FILE_SIZE_LIMIT = "5242880"
|
||||||
|
|
||||||
|
# Rate limiting
|
||||||
|
API_KEY_RATE_LIMIT = "60/minute"
|
||||||
|
}
|
||||||
|
command = ["./bin/docker-entrypoint-worker.sh"]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.worker.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "password" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
module "postgres" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "postgres"
|
||||||
|
image = "docker.io/postgres:17"
|
||||||
|
ports = ["5432:5432"]
|
||||||
|
volumes = ["/opt/storage/data/postgres:/var/lib/postgresql/data:Z"]
|
||||||
|
environment = {
|
||||||
|
POSTGRES_PASSWORD = random_password.password.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.postgres.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.postgres.installed]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "password" {
|
||||||
|
value = random_password.password.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
# PostgreSQL Tenant Module
|
||||||
|
|
||||||
|
This module creates a PostgreSQL database and user (tenant) on a VPS-hosted PostgreSQL instance using SSH tunneling for secure access.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Creates SSH tunnel to VPS-hosted PostgreSQL
|
||||||
|
- Creates isolated database and user for tenant
|
||||||
|
- Generates secure random password
|
||||||
|
- Outputs connection details for application use
|
||||||
|
- Supports explicit dependencies via `wait_on`
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- SSH access to VPS with key-based authentication
|
||||||
|
- PostgreSQL admin credentials
|
||||||
|
- `terraform-ssh-tunnel` module (automatically downloaded)
|
||||||
|
- `cyrilgdn/postgresql` provider (automatically configured)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Example
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "app_tenant" {
|
||||||
|
source = "./quadlets/modules/postgres/tenant"
|
||||||
|
|
||||||
|
tenant_name = "myapp"
|
||||||
|
admin_username = "postgres"
|
||||||
|
admin_password = var.postgres_admin_password
|
||||||
|
server_ip = "203.0.113.10"
|
||||||
|
ssh_user = "fourlights"
|
||||||
|
|
||||||
|
# Wait for PostgreSQL service to be ready
|
||||||
|
wait_on = module.postgres.installed
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the connection details
|
||||||
|
output "app_db_connection" {
|
||||||
|
value = {
|
||||||
|
host = module.app_tenant.host
|
||||||
|
port = module.app_tenant.port
|
||||||
|
database = module.app_tenant.database
|
||||||
|
username = module.app_tenant.username
|
||||||
|
password = module.app_tenant.password
|
||||||
|
}
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Custom Port
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "custom_tenant" {
|
||||||
|
source = "./quadlets/modules/postgres/tenant"
|
||||||
|
|
||||||
|
tenant_name = "custom_app"
|
||||||
|
admin_username = "postgres"
|
||||||
|
admin_password = var.postgres_admin_password
|
||||||
|
server_ip = "203.0.113.10"
|
||||||
|
target_port = 5433 # Custom PostgreSQL port
|
||||||
|
|
||||||
|
wait_on = module.postgres.installed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Tenants
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "tenant_1" {
|
||||||
|
source = "./quadlets/modules/postgres/tenant"
|
||||||
|
|
||||||
|
tenant_name = "app1"
|
||||||
|
admin_username = "postgres"
|
||||||
|
admin_password = var.postgres_admin_password
|
||||||
|
server_ip = var.vps_ip
|
||||||
|
|
||||||
|
wait_on = module.postgres.installed
|
||||||
|
}
|
||||||
|
|
||||||
|
module "tenant_2" {
|
||||||
|
source = "./quadlets/modules/postgres/tenant"
|
||||||
|
|
||||||
|
tenant_name = "app2"
|
||||||
|
admin_username = "postgres"
|
||||||
|
admin_password = var.postgres_admin_password
|
||||||
|
server_ip = var.vps_ip
|
||||||
|
|
||||||
|
wait_on = module.postgres.installed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Name | Description | Type | Default | Required |
|
||||||
|
|------|-------------|------|---------|----------|
|
||||||
|
| tenant_name | Name of the tenant (used for database name and username) | string | - | yes |
|
||||||
|
| admin_username | PostgreSQL admin username | string | "postgres" | no |
|
||||||
|
| admin_password | PostgreSQL admin password | string | - | yes |
|
||||||
|
| server_ip | VPS server IP address | string | - | yes |
|
||||||
|
| ssh_user | SSH user for connecting to VPS | string | "fourlights" | no |
|
||||||
|
| ssh_private_key_path | Path to SSH private key | string | "~/.ssh/id_rsa" | no |
|
||||||
|
| target_port | PostgreSQL port on VPS | number | 5432 | no |
|
||||||
|
| wait_on | Resources to wait on before creating tenant | any | null | no |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Name | Description | Sensitive |
|
||||||
|
|------|-------------|-----------|
|
||||||
|
| host | PostgreSQL connection host (via SSH tunnel) | no |
|
||||||
|
| port | PostgreSQL connection port (via SSH tunnel) | no |
|
||||||
|
| database | Database name | no |
|
||||||
|
| username | Database username | no |
|
||||||
|
| password | Database password | yes |
|
||||||
|
| connection_string | Full PostgreSQL connection string | yes |
|
||||||
|
| installed | Installation complete flag | no |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **SSH Tunnel**: The module uses `terraform-ssh-tunnel` to create a secure SSH tunnel from your local machine to the VPS PostgreSQL instance
|
||||||
|
2. **Provider Configuration**: The PostgreSQL provider is configured to connect through the tunnel
|
||||||
|
3. **Resource Creation**: Creates a PostgreSQL role (user) and database with appropriate permissions
|
||||||
|
4. **Password Generation**: Generates a cryptographically secure random password
|
||||||
|
5. **Outputs**: Provides connection details for your application to use
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The SSH tunnel is created dynamically when Terraform needs to connect
|
||||||
|
- Ensure your SSH agent is running and has the appropriate key loaded
|
||||||
|
- The database and user will have the same name as `tenant_name`
|
||||||
|
- The user is granted full privileges on their database
|
||||||
|
- Password is stored in Terraform state (sensitive)
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.6"
|
||||||
|
}
|
||||||
|
postgresql = {
|
||||||
|
source = "cyrilgdn/postgresql"
|
||||||
|
version = "~> 1.22"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate random password for the tenant user
|
||||||
|
resource "random_password" "tenant_password" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
min_upper = 2
|
||||||
|
min_lower = 2
|
||||||
|
min_numeric = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create SSH tunnel to PostgreSQL on VPS
|
||||||
|
module "db_tunnel" {
|
||||||
|
source = "flaupretre/tunnel/ssh"
|
||||||
|
version = "2.3.0"
|
||||||
|
|
||||||
|
target_host = "127.0.0.1"
|
||||||
|
target_port = var.target_port
|
||||||
|
|
||||||
|
gateway_host = var.server_ip
|
||||||
|
gateway_user = var.ssh_user
|
||||||
|
gateway_port = 22
|
||||||
|
}
|
||||||
|
|
||||||
|
provider postgresql {
|
||||||
|
alias = "tunnel"
|
||||||
|
host = module.db_tunnel.host
|
||||||
|
port = module.db_tunnel.port
|
||||||
|
username = var.admin_username
|
||||||
|
password = var.admin_password
|
||||||
|
database = "postgres"
|
||||||
|
sslmode = "disable"
|
||||||
|
superuser = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#---- Database
|
||||||
|
|
||||||
|
resource postgresql_database db {
|
||||||
|
provider = postgresql.tunnel
|
||||||
|
|
||||||
|
name = var.tenant_name
|
||||||
|
owner = var.tenant_name
|
||||||
|
|
||||||
|
encoding = "UTF8"
|
||||||
|
lc_collate = "C"
|
||||||
|
lc_ctype = "C"
|
||||||
|
}
|
||||||
|
|
||||||
|
#---- DB user/role
|
||||||
|
|
||||||
|
resource postgresql_role role {
|
||||||
|
provider = postgresql.tunnel
|
||||||
|
|
||||||
|
name = var.tenant_name
|
||||||
|
login = true
|
||||||
|
password = random_password.tenant_password.result
|
||||||
|
}
|
||||||
|
|
||||||
|
#---- Grant
|
||||||
|
# SQL: grant all on <db>.* to <user>@'%';
|
||||||
|
|
||||||
|
resource postgresql_grant grant {
|
||||||
|
provider = postgresql.tunnel
|
||||||
|
|
||||||
|
object_type = "database"
|
||||||
|
database = var.tenant_name
|
||||||
|
role = var.tenant_name
|
||||||
|
privileges = ["ALL"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
output "database" {
|
||||||
|
description = "Database name"
|
||||||
|
value = var.tenant_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "username" {
|
||||||
|
description = "Database username"
|
||||||
|
value = var.tenant_name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "password" {
|
||||||
|
description = "Database password"
|
||||||
|
value = random_password.tenant_password.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "connection_string" {
|
||||||
|
description = "PostgreSQL connection string"
|
||||||
|
value = "postgresql://${var.tenant_name}:${random_password.tenant_password.result}@localhost:${var.target_port}/${var.tenant_name}"
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
description = "Installation complete flag"
|
||||||
|
value = true
|
||||||
|
depends_on = [postgresql_role.role, postgresql_grant.grant, postgresql_database.db, random_password.tenant_password]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
variable "tenant_name" {
|
||||||
|
description = "Name of the tenant (used for database name and username)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "admin_username" {
|
||||||
|
description = "PostgreSQL admin username"
|
||||||
|
type = string
|
||||||
|
default = "postgres"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "admin_password" {
|
||||||
|
description = "PostgreSQL admin password"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
description = "VPS server IP address"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_user" {
|
||||||
|
description = "SSH user for connecting to VPS"
|
||||||
|
type = string
|
||||||
|
default = "fourlights"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
description = "Path to SSH private key for authentication"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "target_port" {
|
||||||
|
description = "PostgreSQL port on VPS"
|
||||||
|
type = number
|
||||||
|
default = 5432
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "wait_on" {
|
||||||
|
description = "Resources to wait on before creating tenant"
|
||||||
|
type = any
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {}
|
||||||
|
|
||||||
|
resource "random_password" "api_key" {
|
||||||
|
length = 64
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
module "qdrant" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "qdrant"
|
||||||
|
image = "docker.io/qdrant/qdrant"
|
||||||
|
ports = ["6333:6333", "6334:6334"]
|
||||||
|
volumes = ["/opt/storage/data/qdrant:/qdrant/storage:z"]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
"QDRANT__SERVICE__API_KEY" = random_password.api_key.result
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "qdrant"
|
||||||
|
domain = "qdrant.${var.server_domain}"
|
||||||
|
port = "6333"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
#grpc_port = "6334"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.qdrant.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.qdrant.installed]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "api_key" {
|
||||||
|
value = random_password.api_key.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,7 @@ variable "haproxy_services" {
|
||||||
port = string
|
port = string
|
||||||
host = optional(string, "127.0.0.1")
|
host = optional(string, "127.0.0.1")
|
||||||
tls = optional(bool, false)
|
tls = optional(bool, false)
|
||||||
|
grpc_port = optional(string, "")
|
||||||
}))
|
}))
|
||||||
default = []
|
default = []
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +86,8 @@ locals {
|
||||||
"Label=haproxy.${svc.name}.domain=${svc.domain}",
|
"Label=haproxy.${svc.name}.domain=${svc.domain}",
|
||||||
"Label=haproxy.${svc.name}.port=${svc.port}",
|
"Label=haproxy.${svc.name}.port=${svc.port}",
|
||||||
"Label=haproxy.${svc.name}.host=${svc.host}",
|
"Label=haproxy.${svc.name}.host=${svc.host}",
|
||||||
"Label=haproxy.${svc.name}.tls=${svc.tls}"
|
"Label=haproxy.${svc.name}.tls=${svc.tls}",
|
||||||
|
#"Label=haproxy.${svc.name}.grpc_port=${svc.grpc_port}"
|
||||||
]
|
]
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
@ -109,11 +111,8 @@ resource "null_resource" "deploy_quadlet_app" {
|
||||||
provisioner "remote-exec" {
|
provisioner "remote-exec" {
|
||||||
inline = compact(flatten([
|
inline = compact(flatten([
|
||||||
[
|
[
|
||||||
# Wait for cloud-init to complete before proceeding
|
|
||||||
"cloud-init status --wait || true",
|
|
||||||
|
|
||||||
# Verify the user systemd session is ready and linger is enabled
|
# Verify the user systemd session is ready and linger is enabled
|
||||||
"timeout 60 bash -c 'until loginctl show-user fourlights | grep -q \"Linger=yes\"; do sleep 2; done'",
|
#"timeout 60 bash -c 'until loginctl show-user fourlights | grep -q \"Linger=yes\"; do sleep 2; done'",
|
||||||
|
|
||||||
# Create base quadlet file
|
# Create base quadlet file
|
||||||
"cat > /tmp/${var.app_name}.container << 'EOF'",
|
"cat > /tmp/${var.app_name}.container << 'EOF'",
|
||||||
|
|
@ -123,6 +122,8 @@ resource "null_resource" "deploy_quadlet_app" {
|
||||||
"",
|
"",
|
||||||
"[Container]",
|
"[Container]",
|
||||||
"Image=${var.image}",
|
"Image=${var.image}",
|
||||||
|
"AutoUpdate=registry",
|
||||||
|
"Network=containers.network"
|
||||||
],
|
],
|
||||||
|
|
||||||
# Add ports (only if not empty)
|
# Add ports (only if not empty)
|
||||||
|
|
@ -139,6 +140,7 @@ resource "null_resource" "deploy_quadlet_app" {
|
||||||
|
|
||||||
# Add pre-computed HAProxy labels (only if not empty)
|
# Add pre-computed HAProxy labels (only if not empty)
|
||||||
length(local.haproxy_labels) > 0 ? local.haproxy_labels : [],
|
length(local.haproxy_labels) > 0 ? local.haproxy_labels : [],
|
||||||
|
|
||||||
# Add health checks if not empty
|
# Add health checks if not empty
|
||||||
var.healthcmd != "" ? ["HealthCmd=${var.healthcmd}"] : [],
|
var.healthcmd != "" ? ["HealthCmd=${var.healthcmd}"] : [],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
variable "wait_on" {
|
|
||||||
type = any
|
|
||||||
description = "Resources to wait on"
|
|
||||||
default = true
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "server_ip" {
|
|
||||||
type = string
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "ssh_private_key_path" {
|
|
||||||
type = string
|
|
||||||
}
|
|
||||||
|
|
||||||
module "redis" {
|
|
||||||
source = "../quadlet-app"
|
|
||||||
wait_on = var.wait_on
|
|
||||||
|
|
||||||
server_ip = var.server_ip
|
|
||||||
ssh_private_key_path = var.ssh_private_key_path
|
|
||||||
|
|
||||||
app_name = "redis"
|
|
||||||
image = "docker.io/redis:7-alpine"
|
|
||||||
ports = ["6379:6379"]
|
|
||||||
volumes = ["/opt/storage/data/redis:/data:Z"]
|
|
||||||
command = ["redis-server", "--appendonly", "yes"]
|
|
||||||
}
|
|
||||||
|
|
||||||
output "app_urls" {
|
|
||||||
value = module.redis.app_urls
|
|
||||||
}
|
|
||||||
|
|
||||||
output "installed" {
|
|
||||||
value = true
|
|
||||||
depends_on = [module.redis.installed]
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "username" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "random_password" "password" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
}
|
||||||
|
|
||||||
|
module "rabbitmq" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "rabbitmq"
|
||||||
|
image = "docker.io/rabbitmq:management"
|
||||||
|
ports = ["5672:5672", "15672:15672"]
|
||||||
|
volumes = ["/opt/storage/data/rabbitmq:/var/lib/rabbitmq:Z"]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
RABBITMQ_DEFAULT_USER = random_password.username.result
|
||||||
|
RABBITMQ_DEFAULT_PASS = random_password.password.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.rabbitmq.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.rabbitmq.installed]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "username" {
|
||||||
|
value = random_password.username.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "password" {
|
||||||
|
value = random_password.password.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
# RabbitMQ Tenant Module
|
||||||
|
|
||||||
|
This module creates a RabbitMQ virtual host (vhost) and user (tenant) on a VPS-hosted RabbitMQ instance using SSH tunneling for secure access to the Management API.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Creates SSH tunnel to VPS-hosted RabbitMQ Management API
|
||||||
|
- Creates isolated virtual host and user for tenant
|
||||||
|
- Configures full permissions for user on their vhost
|
||||||
|
- Generates secure random password
|
||||||
|
- Outputs connection details for application use
|
||||||
|
- Supports explicit dependencies via `wait_on`
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- SSH access to VPS with key-based authentication
|
||||||
|
- RabbitMQ admin credentials
|
||||||
|
- RabbitMQ Management plugin enabled
|
||||||
|
- `terraform-ssh-tunnel` module (automatically downloaded)
|
||||||
|
- `cyrilgdn/rabbitmq` provider (automatically configured)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Example
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "app_tenant" {
|
||||||
|
source = "./quadlets/modules/rabbitmq/tenant"
|
||||||
|
|
||||||
|
tenant_name = "myapp"
|
||||||
|
admin_username = var.rabbitmq_admin_username
|
||||||
|
admin_password = var.rabbitmq_admin_password
|
||||||
|
server_ip = "203.0.113.10"
|
||||||
|
ssh_user = "fourlights"
|
||||||
|
|
||||||
|
# Wait for RabbitMQ service to be ready
|
||||||
|
wait_on = module.rabbitmq.installed
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the connection details
|
||||||
|
output "app_mq_connection" {
|
||||||
|
value = {
|
||||||
|
host = module.app_tenant.host
|
||||||
|
port = module.app_tenant.port
|
||||||
|
vhost = module.app_tenant.vhost
|
||||||
|
username = module.app_tenant.username
|
||||||
|
password = module.app_tenant.password
|
||||||
|
}
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Custom Management Port
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "custom_tenant" {
|
||||||
|
source = "./quadlets/modules/rabbitmq/tenant"
|
||||||
|
|
||||||
|
tenant_name = "custom_app"
|
||||||
|
admin_username = var.rabbitmq_admin_username
|
||||||
|
admin_password = var.rabbitmq_admin_password
|
||||||
|
server_ip = "203.0.113.10"
|
||||||
|
target_port = 25672 # Custom Management API port
|
||||||
|
|
||||||
|
wait_on = module.rabbitmq.installed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Tenants
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "tenant_1" {
|
||||||
|
source = "./quadlets/modules/rabbitmq/tenant"
|
||||||
|
|
||||||
|
tenant_name = "app1"
|
||||||
|
admin_username = var.rabbitmq_admin_username
|
||||||
|
admin_password = var.rabbitmq_admin_password
|
||||||
|
server_ip = var.vps_ip
|
||||||
|
|
||||||
|
wait_on = module.rabbitmq.installed
|
||||||
|
}
|
||||||
|
|
||||||
|
module "tenant_2" {
|
||||||
|
source = "./quadlets/modules/rabbitmq/tenant"
|
||||||
|
|
||||||
|
tenant_name = "app2"
|
||||||
|
admin_username = var.rabbitmq_admin_username
|
||||||
|
admin_password = var.rabbitmq_admin_password
|
||||||
|
server_ip = var.vps_ip
|
||||||
|
|
||||||
|
wait_on = module.rabbitmq.installed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Connection String
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
module "app_tenant" {
|
||||||
|
source = "./quadlets/modules/rabbitmq/tenant"
|
||||||
|
|
||||||
|
tenant_name = "myapp"
|
||||||
|
admin_username = var.rabbitmq_admin_username
|
||||||
|
admin_password = var.rabbitmq_admin_password
|
||||||
|
server_ip = var.vps_ip
|
||||||
|
|
||||||
|
wait_on = module.rabbitmq.installed
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use in application configuration
|
||||||
|
resource "kubernetes_secret" "rabbitmq_credentials" {
|
||||||
|
metadata {
|
||||||
|
name = "rabbitmq-connection"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
connection_string = module.app_tenant.connection_string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Name | Description | Type | Default | Required |
|
||||||
|
|------|-------------|------|---------|----------|
|
||||||
|
| tenant_name | Name of the tenant (used for vhost name and username) | string | - | yes |
|
||||||
|
| admin_username | RabbitMQ admin username | string | - | yes |
|
||||||
|
| admin_password | RabbitMQ admin password | string | - | yes |
|
||||||
|
| server_ip | VPS server IP address | string | - | yes |
|
||||||
|
| ssh_user | SSH user for connecting to VPS | string | "fourlights" | no |
|
||||||
|
| ssh_private_key_path | Path to SSH private key | string | "~/.ssh/id_rsa" | no |
|
||||||
|
| target_port | RabbitMQ Management API port on VPS | number | 15672 | no |
|
||||||
|
| wait_on | Resources to wait on before creating tenant | any | null | no |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
| Name | Description | Sensitive |
|
||||||
|
|------|-------------|-----------|
|
||||||
|
| host | RabbitMQ connection host (VPS IP for AMQP) | no |
|
||||||
|
| port | RabbitMQ AMQP connection port (5672) | no |
|
||||||
|
| management_host | RabbitMQ Management API host (via SSH tunnel) | no |
|
||||||
|
| management_port | RabbitMQ Management API port (via SSH tunnel) | no |
|
||||||
|
| vhost | Virtual host name | no |
|
||||||
|
| username | RabbitMQ username | no |
|
||||||
|
| password | RabbitMQ password | yes |
|
||||||
|
| connection_string | Full AMQP connection string | yes |
|
||||||
|
| installed | Installation complete flag | no |
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **SSH Tunnel**: The module uses `terraform-ssh-tunnel` to create a secure SSH tunnel from your local machine to the VPS RabbitMQ Management API (port 15672)
|
||||||
|
2. **Provider Configuration**: The RabbitMQ provider is configured to connect through the tunnel
|
||||||
|
3. **Resource Creation**: Creates a RabbitMQ virtual host, user, and permissions
|
||||||
|
4. **Password Generation**: Generates a cryptographically secure random password
|
||||||
|
5. **Outputs**: Provides connection details for your application to use
|
||||||
|
|
||||||
|
## Connection Details
|
||||||
|
|
||||||
|
- **Management API**: Accessed via SSH tunnel (for Terraform operations)
|
||||||
|
- **AMQP Protocol**: Direct connection to VPS on port 5672 (for application connections)
|
||||||
|
- **Virtual Host**: Isolated messaging environment for the tenant
|
||||||
|
- **Permissions**: User has full configure, write, and read permissions on their vhost
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The SSH tunnel is only used for management operations (creating vhost/user/permissions)
|
||||||
|
- Applications connect directly to the VPS AMQP port (5672), not through the tunnel
|
||||||
|
- The virtual host and user will have the same name as `tenant_name`
|
||||||
|
- User is granted `configure=".*"`, `write=".*"`, `read=".*"` on their vhost
|
||||||
|
- Password is stored in Terraform state (sensitive)
|
||||||
|
- Ensure RabbitMQ Management plugin is enabled on the VPS instance
|
||||||
|
|
||||||
|
## Permissions Explained
|
||||||
|
|
||||||
|
The tenant user receives the following permissions on their vhost:
|
||||||
|
|
||||||
|
- **configure**: Can declare exchanges, queues, and bindings
|
||||||
|
- **write**: Can publish messages
|
||||||
|
- **read**: Can consume messages
|
||||||
|
|
||||||
|
All permissions use the `.*` regex pattern, granting full access within the vhost.
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
random = {
|
||||||
|
source = "hashicorp/random"
|
||||||
|
version = "~> 3.6"
|
||||||
|
}
|
||||||
|
rabbitmq = {
|
||||||
|
source = "cyrilgdn/rabbitmq"
|
||||||
|
version = "~> 1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate random password for the tenant user
|
||||||
|
resource "random_password" "tenant_password" {
|
||||||
|
length = 32
|
||||||
|
special = false
|
||||||
|
min_upper = 2
|
||||||
|
min_lower = 2
|
||||||
|
min_numeric = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create SSH tunnel to RabbitMQ Management API on VPS
|
||||||
|
module "ssh_tunnel" {
|
||||||
|
source = "flaupretre/tunnel/ssh"
|
||||||
|
version = "2.2.1"
|
||||||
|
|
||||||
|
target_host = "127.0.0.1"
|
||||||
|
target_port = var.target_port
|
||||||
|
|
||||||
|
gateway_host = var.server_ip
|
||||||
|
gateway_user = var.ssh_user
|
||||||
|
|
||||||
|
# Tunnel will be created when needed
|
||||||
|
timeout = "30s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configure RabbitMQ provider to use the SSH tunnel
|
||||||
|
provider "rabbitmq" {
|
||||||
|
endpoint = "http://${module.ssh_tunnel.host}:${module.ssh_tunnel.port}"
|
||||||
|
username = var.admin_username
|
||||||
|
password = var.admin_password
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the virtual host
|
||||||
|
resource "rabbitmq_vhost" "tenant_vhost" {
|
||||||
|
depends_on = [var.wait_on]
|
||||||
|
|
||||||
|
name = var.tenant_name
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create the user
|
||||||
|
resource "rabbitmq_user" "tenant_user" {
|
||||||
|
depends_on = [var.wait_on]
|
||||||
|
|
||||||
|
name = var.tenant_name
|
||||||
|
password = random_password.tenant_password.result
|
||||||
|
tags = []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grant permissions to the user on the vhost
|
||||||
|
resource "rabbitmq_permissions" "tenant_permissions" {
|
||||||
|
user = rabbitmq_user.tenant_user.name
|
||||||
|
vhost = rabbitmq_vhost.tenant_vhost.name
|
||||||
|
|
||||||
|
permissions {
|
||||||
|
configure = ".*"
|
||||||
|
write = ".*"
|
||||||
|
read = ".*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
output "host" {
|
||||||
|
description = "RabbitMQ connection host (via SSH tunnel)"
|
||||||
|
value = var.server_ip
|
||||||
|
}
|
||||||
|
|
||||||
|
output "port" {
|
||||||
|
description = "RabbitMQ AMQP connection port"
|
||||||
|
value = 5672
|
||||||
|
}
|
||||||
|
|
||||||
|
output "management_host" {
|
||||||
|
description = "RabbitMQ Management API host (via SSH tunnel)"
|
||||||
|
value = module.ssh_tunnel.host
|
||||||
|
}
|
||||||
|
|
||||||
|
output "management_port" {
|
||||||
|
description = "RabbitMQ Management API port (via SSH tunnel)"
|
||||||
|
value = module.ssh_tunnel.port
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vhost" {
|
||||||
|
description = "Virtual host name"
|
||||||
|
value = rabbitmq_vhost.tenant_vhost.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "username" {
|
||||||
|
description = "RabbitMQ username"
|
||||||
|
value = rabbitmq_user.tenant_user.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "password" {
|
||||||
|
description = "RabbitMQ password"
|
||||||
|
value = random_password.tenant_password.result
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "connection_string" {
|
||||||
|
description = "RabbitMQ AMQP connection string"
|
||||||
|
value = "amqp://${rabbitmq_user.tenant_user.name}:${random_password.tenant_password.result}@${var.server_ip}:5672/${rabbitmq_vhost.tenant_vhost.name}"
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
description = "Installation complete flag"
|
||||||
|
value = true
|
||||||
|
depends_on = [rabbitmq_vhost.tenant_vhost, rabbitmq_user.tenant_user, rabbitmq_permissions.tenant_permissions]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
variable "tenant_name" {
|
||||||
|
description = "Name of the tenant (used for vhost name and username)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "admin_username" {
|
||||||
|
description = "RabbitMQ admin username"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "admin_password" {
|
||||||
|
description = "RabbitMQ admin password"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
description = "VPS server IP address"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_user" {
|
||||||
|
description = "SSH user for connecting to VPS"
|
||||||
|
type = string
|
||||||
|
default = "fourlights"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
description = "Path to SSH private key for authentication"
|
||||||
|
type = string
|
||||||
|
default = "~/.ssh/id_rsa"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "target_port" {
|
||||||
|
description = "RabbitMQ Management API port on VPS"
|
||||||
|
type = number
|
||||||
|
default = 15672
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "wait_on" {
|
||||||
|
description = "Resources to wait on before creating tenant"
|
||||||
|
type = any
|
||||||
|
default = null
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_url" {
|
||||||
|
type = string
|
||||||
|
default = "https://mail.binarysunset.dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
# tmail-web reads its configuration from a file at
|
||||||
|
# /usr/share/nginx/html/assets/env.file inside the container. Setting
|
||||||
|
# SERVER_URL as a container environment variable does NOT work (see
|
||||||
|
# https://github.com/linagora/tmail-flutter/issues/4240); the file must
|
||||||
|
# be injected. We write it to the host and bind-mount it into the
|
||||||
|
# container.
|
||||||
|
locals {
|
||||||
|
env_file_host_path = "/opt/storage/data/tmail-web/env.file"
|
||||||
|
env_file_container_path = "/usr/share/nginx/html/assets/env.file"
|
||||||
|
env_file_content = "SERVER_URL=${var.server_url}\nDOMAIN_REDIRECT_URL=https://mail.${var.server_domain}\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "null_resource" "tmail_env_file" {
|
||||||
|
triggers = {
|
||||||
|
content = local.env_file_content
|
||||||
|
server_ip = var.server_ip
|
||||||
|
host_path = local.env_file_host_path
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = [
|
||||||
|
"mkdir -p $(dirname ${local.env_file_host_path})",
|
||||||
|
"cat > ${local.env_file_host_path} << 'EOF'",
|
||||||
|
local.env_file_content,
|
||||||
|
"EOF",
|
||||||
|
]
|
||||||
|
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = var.server_ip
|
||||||
|
user = "fourlights"
|
||||||
|
agent = true
|
||||||
|
agent_identity = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "tmail-web" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "tmail-web"
|
||||||
|
image = "docker.io/linagora/tmail-web:latest"
|
||||||
|
ports = ["8080:80"]
|
||||||
|
volumes = ["${local.env_file_host_path}:${local.env_file_container_path}:ro"]
|
||||||
|
|
||||||
|
environment = {}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "tmail-web"
|
||||||
|
domain = "mail.${var.server_domain}"
|
||||||
|
port = "8080"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
healthcmd = "curl -f http://localhost:80 || exit 1"
|
||||||
|
|
||||||
|
depends_on = [null_resource.tmail_env_file]
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.tmail-web.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.tmail-web.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
module "zot" {
|
||||||
|
source = "../quadlet-app"
|
||||||
|
wait_on = var.wait_on
|
||||||
|
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
app_name = "zot"
|
||||||
|
image = "ghcr.io/project-zot/zot:latest"
|
||||||
|
ports = ["5000:5000"]
|
||||||
|
volumes = [
|
||||||
|
"/opt/storage/data/zot/config.json:/etc/zot/config.json:Z",
|
||||||
|
"/opt/storage/data/zot:/var/lib/registry:Z",
|
||||||
|
]
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
ND_LOGLEVEL="info"
|
||||||
|
ND_ENABLEINSIGHTSCOLLECTOR="false"
|
||||||
|
}
|
||||||
|
|
||||||
|
haproxy_services = [
|
||||||
|
{
|
||||||
|
name = "registry"
|
||||||
|
domain = "registry.${var.server_domain}"
|
||||||
|
port = "5000"
|
||||||
|
host = "127.0.0.1"
|
||||||
|
tls = false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
output "app_urls" {
|
||||||
|
value = module.zot.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [module.zot.installed]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
variable "wait_on" {
|
||||||
|
type = any
|
||||||
|
description = "Resources to wait on"
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_ip" {
|
||||||
|
description = "Target server IP"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
description = "Path to SSH private key"
|
||||||
|
type = string
|
||||||
|
default = "~/.ssh/id_rsa"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "name" {
|
||||||
|
description = "Name of the network"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "null_resource" "deploy_network" {
|
||||||
|
depends_on = [var.wait_on]
|
||||||
|
triggers = {
|
||||||
|
name = var.name
|
||||||
|
server_ip = var.server_ip
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = compact(flatten([
|
||||||
|
[
|
||||||
|
|
||||||
|
# Create base quadlet file
|
||||||
|
"cat > /tmp/${var.name}.network << 'EOF'",
|
||||||
|
"[Network]",
|
||||||
|
"Description=${var.name} network",
|
||||||
|
"",
|
||||||
|
"[Install]",
|
||||||
|
"WantedBy=default.target",
|
||||||
|
"EOF",
|
||||||
|
|
||||||
|
"test -d ~/.config/containers/systemd || mkdir -p ~/.config/containers/systemd",
|
||||||
|
"cp /tmp/${var.name}.network ~/.config/containers/systemd/${var.name}.network",
|
||||||
|
"systemctl --user daemon-reload",
|
||||||
|
]
|
||||||
|
]))
|
||||||
|
|
||||||
|
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = var.server_ip
|
||||||
|
user = "fourlights"
|
||||||
|
agent = true
|
||||||
|
agent_identity = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
when = destroy
|
||||||
|
inline = [
|
||||||
|
# Remove the .container file
|
||||||
|
"rm -f ~/.config/containers/systemd/${self.triggers.name}.network",
|
||||||
|
|
||||||
|
# Reload systemd to remove the generated service
|
||||||
|
"systemctl --user daemon-reload",
|
||||||
|
|
||||||
|
# Force remove any lingering containers
|
||||||
|
"podman network rm -f ${self.triggers.name} || true"
|
||||||
|
]
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = self.triggers.server_ip
|
||||||
|
user = "fourlights"
|
||||||
|
agent = true
|
||||||
|
agent_identity = self.triggers.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "name" {
|
||||||
|
value = var.name
|
||||||
|
}
|
||||||
|
|
||||||
|
output "service_status" {
|
||||||
|
value = "${var.name} deployed"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "installed" {
|
||||||
|
value = true
|
||||||
|
depends_on = [null_resource.deploy_network]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
.idea
|
||||||
|
.venv/
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
||||||
|
|
@ -0,0 +1,625 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
HAPROXY_API_BASE = "http://:admin@127.0.0.1:5555/v3"
|
||||||
|
CERT_DIR = "/home/fourlights/.acme.sh"
|
||||||
|
ACME_SCRIPT = "/usr/local/bin/acme.sh"
|
||||||
|
|
||||||
|
|
||||||
|
class PodmanHAProxyACMESync:
|
||||||
|
def __init__(self):
|
||||||
|
self.ssl_services = set()
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({"Content-Type": "application/json"})
|
||||||
|
|
||||||
|
def get_next_index(self, path):
|
||||||
|
response = self.session.get(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/{path}"
|
||||||
|
)
|
||||||
|
return len(response.json()) if response.status_code == 200 else None
|
||||||
|
|
||||||
|
def get_dataplaneapi_version(self):
|
||||||
|
response = self.session.get(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/version"
|
||||||
|
)
|
||||||
|
return response.json() if response.status_code == 200 else None
|
||||||
|
|
||||||
|
def get_container_labels(self, container_id):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["podman", "inspect", container_id], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
return data[0]["Config"]["Labels"] or {}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting labels for {container_id}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# ===== PHASE 1: Helper Methods (Check/Query Operations) =====
|
||||||
|
|
||||||
|
def check_acl_exists(self, frontend, acl_name, criterion, value):
|
||||||
|
"""
|
||||||
|
Check if an ACL exists in the specified frontend by matching name, criterion, and value.
|
||||||
|
Returns: (exists: bool, index: int | None, current_config: dict | None)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.session.get(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/acls"
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
acls = response.json()
|
||||||
|
for idx, acl in enumerate(acls):
|
||||||
|
if (acl.get("acl_name") == acl_name and
|
||||||
|
acl.get("criterion") == criterion and
|
||||||
|
acl.get("value") == value):
|
||||||
|
return (True, idx, acl)
|
||||||
|
return (False, None, None)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Error checking ACL existence in frontend {frontend}: {e}")
|
||||||
|
return (False, None, None)
|
||||||
|
|
||||||
|
def check_backend_rule_exists(self, frontend, rule_name, cond_test):
|
||||||
|
"""
|
||||||
|
Check if a backend switching rule exists in the specified frontend by matching name and condition test.
|
||||||
|
Returns: (exists: bool, index: int | None, current_config: dict | None)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.session.get(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/backend_switching_rules"
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
rules = response.json()
|
||||||
|
for idx, rule in enumerate(rules):
|
||||||
|
if (rule.get("name") == rule_name and
|
||||||
|
rule.get("cond_test") == cond_test):
|
||||||
|
return (True, idx, rule)
|
||||||
|
return (False, None, None)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Error checking backend rule existence in frontend {frontend}: {e}")
|
||||||
|
return (False, None, None)
|
||||||
|
|
||||||
|
def check_http_request_rule_exists(self, frontend, cond_test, redirect_type, redirect_value):
|
||||||
|
"""
|
||||||
|
Check if an http_request_rule (redirect rule) exists by matching condition test and redirect parameters.
|
||||||
|
Returns: (exists: bool, index: int | None, current_config: dict | None)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.session.get(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/http_request_rules"
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
rules = response.json()
|
||||||
|
for idx, rule in enumerate(rules):
|
||||||
|
if (rule.get("cond_test") == cond_test and
|
||||||
|
rule.get("type") == "redirect"):
|
||||||
|
redirect_cfg = rule.get("redirect_rule", {})
|
||||||
|
if (redirect_cfg.get("type") == redirect_type and
|
||||||
|
redirect_cfg.get("value") == redirect_value):
|
||||||
|
return (True, idx, rule)
|
||||||
|
return (False, None, None)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Error checking http_request_rule existence in frontend {frontend}: {e}")
|
||||||
|
return (False, None, None)
|
||||||
|
|
||||||
|
# ===== PHASE 2: Upsert Operations =====
|
||||||
|
|
||||||
|
def upsert_acl(self, frontend, acl_name, criterion, value):
|
||||||
|
"""
|
||||||
|
Create or update an ACL in the specified frontend.
|
||||||
|
Returns: (success: bool, operation_type: str)
|
||||||
|
- operation_type: "CREATE", "UPDATE", "SKIP", or "ERROR"
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
exists, idx, current_config = self.check_acl_exists(frontend, acl_name, criterion, value)
|
||||||
|
|
||||||
|
if exists and current_config:
|
||||||
|
# ACL exists with same config - skip
|
||||||
|
print(f"[UPSERT-SKIP] ACL {acl_name} already exists in frontend {frontend}, skipping")
|
||||||
|
return (True, "SKIP")
|
||||||
|
elif exists and idx is not None:
|
||||||
|
# ACL exists but with different config - delete and recreate
|
||||||
|
try:
|
||||||
|
self.session.delete(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/acls/{idx}?version={self.get_dataplaneapi_version()}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Failed to delete old ACL {acl_name}: {e}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
|
||||||
|
# Create new ACL
|
||||||
|
acl_data = {
|
||||||
|
"acl_name": acl_name,
|
||||||
|
"criterion": criterion,
|
||||||
|
"value": value,
|
||||||
|
}
|
||||||
|
response = self.session.post(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/acls/{self.get_next_index(f'frontends/{frontend}/acls')}?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=acl_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
op_type = "UPDATE" if (exists and idx is not None) else "CREATE"
|
||||||
|
print(f"[UPSERT-{op_type}] ACL {acl_name} {op_type.lower()}d in frontend {frontend}")
|
||||||
|
return (True, op_type)
|
||||||
|
else:
|
||||||
|
print(f"[UPSERT-ERROR] Failed to create ACL {acl_name}: {response.status_code} - {response.text}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Exception upserting ACL {acl_name}: {e}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
|
||||||
|
def upsert_backend_rule(self, frontend, rule_name, cond_test):
|
||||||
|
"""
|
||||||
|
Create or update a backend switching rule in the specified frontend.
|
||||||
|
Returns: (success: bool, operation_type: str)
|
||||||
|
- operation_type: "CREATE", "UPDATE", "SKIP", or "ERROR"
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
exists, idx, current_config = self.check_backend_rule_exists(frontend, rule_name, cond_test)
|
||||||
|
|
||||||
|
if exists and current_config:
|
||||||
|
# Rule exists with same config - skip
|
||||||
|
print(f"[UPSERT-SKIP] Backend rule {rule_name} already exists in frontend {frontend}, skipping")
|
||||||
|
return (True, "SKIP")
|
||||||
|
elif exists and idx is not None:
|
||||||
|
# Rule exists but with different config - delete and recreate
|
||||||
|
try:
|
||||||
|
self.session.delete(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/backend_switching_rules/{idx}?version={self.get_dataplaneapi_version()}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Failed to delete old backend rule {rule_name}: {e}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
|
||||||
|
# Create new backend rule
|
||||||
|
rule_data = {
|
||||||
|
"name": rule_name,
|
||||||
|
"cond": "if",
|
||||||
|
"cond_test": cond_test,
|
||||||
|
}
|
||||||
|
response = self.session.post(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/backend_switching_rules/{self.get_next_index(f'frontends/{frontend}/backend_switching_rules')}?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=rule_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
op_type = "UPDATE" if (exists and idx is not None) else "CREATE"
|
||||||
|
print(f"[UPSERT-{op_type}] Backend rule {rule_name} {op_type.lower()}d in frontend {frontend}")
|
||||||
|
return (True, op_type)
|
||||||
|
else:
|
||||||
|
print(f"[UPSERT-ERROR] Failed to create backend rule {rule_name}: {response.status_code} - {response.text}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Exception upserting backend rule {rule_name}: {e}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
|
||||||
|
def upsert_http_request_rule(self, frontend, cond_test, redirect_rule_data):
|
||||||
|
"""
|
||||||
|
Create or update an http_request_rule (redirect rule) in the specified frontend.
|
||||||
|
redirect_rule_data should contain: {"type": "redirect", "redirect_rule": {...}, "cond": "if", "cond_test": ...}
|
||||||
|
Returns: (success: bool, operation_type: str)
|
||||||
|
- operation_type: "CREATE", "UPDATE", "SKIP", or "ERROR"
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
redirect_type = redirect_rule_data.get("redirect_rule", {}).get("type")
|
||||||
|
redirect_value = redirect_rule_data.get("redirect_rule", {}).get("value")
|
||||||
|
|
||||||
|
exists, idx, current_config = self.check_http_request_rule_exists(
|
||||||
|
frontend, cond_test, redirect_type, redirect_value
|
||||||
|
)
|
||||||
|
|
||||||
|
if exists and current_config:
|
||||||
|
# Rule exists with same config - skip
|
||||||
|
print(f"[UPSERT-SKIP] Redirect rule for {cond_test} already exists in frontend {frontend}, skipping")
|
||||||
|
return (True, "SKIP")
|
||||||
|
elif exists and idx is not None:
|
||||||
|
# Rule exists but with different config - delete and recreate
|
||||||
|
try:
|
||||||
|
self.session.delete(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/http_request_rules/{idx}?version={self.get_dataplaneapi_version()}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Failed to delete old redirect rule for {cond_test}: {e}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
|
||||||
|
# Create new http_request_rule
|
||||||
|
response = self.session.post(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/{frontend}/http_request_rules/{self.get_next_index(f'frontends/{frontend}/http_request_rules')}?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=redirect_rule_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
op_type = "UPDATE" if (exists and idx is not None) else "CREATE"
|
||||||
|
print(f"[UPSERT-{op_type}] Redirect rule for {cond_test} {op_type.lower()}d in frontend {frontend}")
|
||||||
|
return (True, op_type)
|
||||||
|
else:
|
||||||
|
print(f"[UPSERT-ERROR] Failed to create redirect rule for {cond_test}: {response.status_code} - {response.text}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UPSERT-ERROR] Exception upserting redirect rule for {cond_test}: {e}")
|
||||||
|
return (False, "ERROR")
|
||||||
|
|
||||||
|
def request_certificate(self, domain):
|
||||||
|
print(f"[CERT-REQUEST] About to request certificate for {domain}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
ACME_SCRIPT,
|
||||||
|
"--issue",
|
||||||
|
"-d",
|
||||||
|
domain,
|
||||||
|
"--standalone",
|
||||||
|
"--httpport",
|
||||||
|
"8888",
|
||||||
|
"--server",
|
||||||
|
"letsencrypt",
|
||||||
|
"--listen-v4",
|
||||||
|
"--debug",
|
||||||
|
"2",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Log the command being executed
|
||||||
|
print(f"[CERT-REQUEST] Executing: {' '.join(cmd)}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
# Log both stdout and stderr for complete debugging
|
||||||
|
if result.stdout:
|
||||||
|
print(f"[CERT-STDOUT] {result.stdout}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
if result.stderr:
|
||||||
|
print(f"[CERT-STDERR] {result.stderr}")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"[CERT-SUCCESS] Certificate obtained for {domain}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.install_certificate(domain)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"[CERT-FAILED] Failed to obtain certificate for {domain}")
|
||||||
|
print(f"[CERT-FAILED] Return code: {result.returncode}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CERT-ERROR] Error requesting certificate: {e}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def install_certificate(self, domain):
|
||||||
|
cert_file = f"{CERT_DIR}/{domain}.pem"
|
||||||
|
|
||||||
|
try:
|
||||||
|
acme_cert_dir = f"/home/fourlights/.acme.sh/{domain}_ecc"
|
||||||
|
|
||||||
|
with open(cert_file, "w") as outfile:
|
||||||
|
with open(f"{acme_cert_dir}/fullchain.cer") as cert:
|
||||||
|
outfile.write(cert.read())
|
||||||
|
with open(f"{acme_cert_dir}/{domain}.key") as key:
|
||||||
|
outfile.write(key.read())
|
||||||
|
try:
|
||||||
|
with open(f"{acme_cert_dir}/ca.cer") as ca:
|
||||||
|
outfile.write(ca.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
os.chmod(cert_file, 0o600)
|
||||||
|
print(f"Certificate installed at {cert_file}")
|
||||||
|
|
||||||
|
self.update_haproxy_ssl_bind(domain)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error installing certificate for {domain}: {e}")
|
||||||
|
|
||||||
|
def update_haproxy_ssl_bind(self, domain):
|
||||||
|
print(f"Updating ssl bind for {domain}")
|
||||||
|
try:
|
||||||
|
ssl_bind_data = {
|
||||||
|
"address": "*",
|
||||||
|
"port": 443,
|
||||||
|
"ssl": True,
|
||||||
|
"ssl_certificate": f"{CERT_DIR}/{domain}.pem",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/https_main/binds?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=ssl_bind_data,
|
||||||
|
)
|
||||||
|
print(response.json())
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
print(f"Updated HAProxy SSL bind for {domain}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating HAProxy SSL bind: {e}")
|
||||||
|
|
||||||
|
def setup_certificate_renewal(self, domain):
|
||||||
|
renewal_script = f"/etc/cron.d/acme-{domain.replace('.', '-')}"
|
||||||
|
|
||||||
|
cron_content = f"""0 0 * * * root {ACME_SCRIPT} --renew -d {domain} --post-hook "systemctl reload haproxy" >/dev/null 2>&1
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(renewal_script, "w") as f:
|
||||||
|
f.write(cron_content)
|
||||||
|
|
||||||
|
print(f"Setup automatic renewal for {domain}")
|
||||||
|
|
||||||
|
def update_haproxy_backend(self, service_name, host, port, action="add"):
|
||||||
|
backend_name = f"backend_{service_name}"
|
||||||
|
server_name = f"{service_name}_server"
|
||||||
|
|
||||||
|
if action == "add":
|
||||||
|
backend_data = {
|
||||||
|
"name": backend_name,
|
||||||
|
"mode": "http",
|
||||||
|
"balance": {"algorithm": "roundrobin"},
|
||||||
|
}
|
||||||
|
backends = self.session.post(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/backends?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=backend_data,
|
||||||
|
)
|
||||||
|
print(backends.json())
|
||||||
|
|
||||||
|
server_data = {
|
||||||
|
"name": server_name,
|
||||||
|
"address": host,
|
||||||
|
"port": int(port),
|
||||||
|
"check": "enabled",
|
||||||
|
}
|
||||||
|
tweak = self.session.post(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/backends/{backend_name}/servers?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=server_data,
|
||||||
|
)
|
||||||
|
print(tweak.json())
|
||||||
|
|
||||||
|
elif action == "remove":
|
||||||
|
self.session.delete(
|
||||||
|
f"{HAPROXY_API_BASE}/services/haproxy/configuration/backends/{backend_name}/servers/{server_name}?version={self.get_dataplaneapi_version()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_haproxy_frontend_rule(
|
||||||
|
self, service_name, domain, ssl_enabled=False, action="add"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update HAProxy frontend rules (ACLs, backend switching rules, and redirect rules).
|
||||||
|
Uses upsert behavior to prevent duplicate rules on multiple syncs.
|
||||||
|
"""
|
||||||
|
if action == "add":
|
||||||
|
if ssl_enabled and domain and domain not in self.ssl_services:
|
||||||
|
print(f"Setting up SSL for {domain}")
|
||||||
|
if self.request_certificate(domain):
|
||||||
|
self.setup_certificate_renewal(domain)
|
||||||
|
self.ssl_services.add(domain)
|
||||||
|
|
||||||
|
# Upsert ACL in HTTP frontend (main)
|
||||||
|
self.upsert_acl(
|
||||||
|
"main",
|
||||||
|
f"is_{service_name}",
|
||||||
|
"hdr(host)",
|
||||||
|
domain
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upsert ACL in HTTPS frontend if SSL enabled
|
||||||
|
if ssl_enabled:
|
||||||
|
self.upsert_acl(
|
||||||
|
"https_main",
|
||||||
|
f"is_{service_name}",
|
||||||
|
"hdr(host)",
|
||||||
|
domain
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upsert backend switching rule in HTTP frontend
|
||||||
|
self.upsert_backend_rule(
|
||||||
|
"main",
|
||||||
|
f"backend_{service_name}",
|
||||||
|
f"is_{service_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upsert backend switching rule in HTTPS frontend if SSL enabled
|
||||||
|
if ssl_enabled:
|
||||||
|
self.upsert_backend_rule(
|
||||||
|
"https_main",
|
||||||
|
f"backend_{service_name}",
|
||||||
|
f"is_{service_name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Upsert redirect rule (HTTP to HTTPS)
|
||||||
|
redirect_rule = {
|
||||||
|
"type": "redirect",
|
||||||
|
"redirect_rule": {"type": "scheme", "value": "https", "code": 301},
|
||||||
|
"cond": "if",
|
||||||
|
"cond_test": f"is_{service_name}",
|
||||||
|
}
|
||||||
|
self.upsert_http_request_rule(
|
||||||
|
"main",
|
||||||
|
f"is_{service_name}",
|
||||||
|
redirect_rule
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_container_event(self, event):
|
||||||
|
# DIAGNOSTIC: Log raw event structure
|
||||||
|
print(
|
||||||
|
f"[EVENT-DEBUG] Received event - Type: {event.get('Type', 'MISSING')}, Action: {event.get('Action', 'MISSING')}"
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# DIAGNOSTIC: Check for Actor key
|
||||||
|
if "Actor" not in event:
|
||||||
|
print(
|
||||||
|
f"[EVENT-SKIP] Skipping event without 'Actor' key - Full event: {json.dumps(event)}"
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# DIAGNOSTIC: Check for ID in Actor
|
||||||
|
if "ID" not in event["Actor"]:
|
||||||
|
print(
|
||||||
|
f"[EVENT-SKIP] Skipping event without 'Actor.ID' - Actor content: {json.dumps(event['Actor'])}"
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
container_id = event["Actor"]["ID"][:12]
|
||||||
|
action = event["Action"]
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[EVENT-PROCESS] Processing '{action}' event for container {container_id}"
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
labels = self.get_container_labels(container_id)
|
||||||
|
|
||||||
|
# Dictionary to store discovered services
|
||||||
|
services = {}
|
||||||
|
|
||||||
|
# First, check for namespaced labels (haproxy.{service_name}.enable)
|
||||||
|
for label_key, label_value in labels.items():
|
||||||
|
if (
|
||||||
|
label_key.startswith("haproxy.")
|
||||||
|
and label_key.endswith(".enable")
|
||||||
|
and label_value.lower() == "true"
|
||||||
|
):
|
||||||
|
# Extract service name from label key
|
||||||
|
parts = label_key.split(".")
|
||||||
|
if len(parts) == 3: # haproxy.{service_name}.enable
|
||||||
|
service_name = parts[1]
|
||||||
|
|
||||||
|
# Extract properties for this service namespace
|
||||||
|
service_config = {
|
||||||
|
"service_name": service_name,
|
||||||
|
"host": labels.get(f"haproxy.{service_name}.host", "127.0.0.1"),
|
||||||
|
"port": labels.get(f"haproxy.{service_name}.port", "8080"),
|
||||||
|
"domain": labels.get(f"haproxy.{service_name}.domain", None),
|
||||||
|
"ssl_enabled": labels.get(
|
||||||
|
f"haproxy.{service_name}.tls", "false"
|
||||||
|
).lower()
|
||||||
|
== "true",
|
||||||
|
}
|
||||||
|
services[service_name] = service_config
|
||||||
|
|
||||||
|
# Backward compatibility: If no namespaced labels found, check for flat labels
|
||||||
|
if (
|
||||||
|
not services
|
||||||
|
and "haproxy.enable" in labels
|
||||||
|
and labels["haproxy.enable"].lower() == "true"
|
||||||
|
):
|
||||||
|
service_name = labels.get("haproxy.service", container_id)
|
||||||
|
services[service_name] = {
|
||||||
|
"service_name": service_name,
|
||||||
|
"host": labels.get("haproxy.host", "127.0.0.1"),
|
||||||
|
"port": labels.get("haproxy.port", "8080"),
|
||||||
|
"domain": labels.get("haproxy.domain", None),
|
||||||
|
"ssl_enabled": labels.get("haproxy.tls", "false").lower() == "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process each discovered service
|
||||||
|
for service_name, config in services.items():
|
||||||
|
if action in ["start", "restart"]:
|
||||||
|
print(
|
||||||
|
f"Adding service {config['service_name']} to HAProxy (SSL: {config['ssl_enabled']}, Domain: {config['domain']})"
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.update_haproxy_backend(
|
||||||
|
config["service_name"], config["host"], config["port"], "add"
|
||||||
|
)
|
||||||
|
if config["domain"]:
|
||||||
|
self.update_haproxy_frontend_rule(
|
||||||
|
config["service_name"],
|
||||||
|
config["domain"],
|
||||||
|
config["ssl_enabled"],
|
||||||
|
"add",
|
||||||
|
)
|
||||||
|
|
||||||
|
elif action in ["stop", "remove", "died"]:
|
||||||
|
print(f"Removing service {config['service_name']} from HAProxy")
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.update_haproxy_backend(
|
||||||
|
config["service_name"], config["host"], config["port"], "remove"
|
||||||
|
)
|
||||||
|
|
||||||
|
def watch_events(self):
|
||||||
|
print("Starting Podman-HAProxy-ACME sync...")
|
||||||
|
|
||||||
|
# Track last sync time
|
||||||
|
last_full_sync = 0
|
||||||
|
SYNC_INTERVAL = 60 # Re-scan all containers every 60 seconds
|
||||||
|
|
||||||
|
def do_full_sync():
|
||||||
|
"""Perform a full sync of all running containers"""
|
||||||
|
print("Performing full container sync...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["podman", "ps", "--format", "json"], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
containers = json.loads(result.stdout)
|
||||||
|
for container in containers:
|
||||||
|
event = {
|
||||||
|
"Type": "container",
|
||||||
|
"Action": "start",
|
||||||
|
"Actor": {"ID": container.get("Id", "")},
|
||||||
|
}
|
||||||
|
self.process_container_event(event)
|
||||||
|
print(f"Synced {len(containers)} containers")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during full sync: {e}")
|
||||||
|
|
||||||
|
# Initial sync
|
||||||
|
do_full_sync()
|
||||||
|
last_full_sync = time.time()
|
||||||
|
|
||||||
|
print("Watching for container events...")
|
||||||
|
|
||||||
|
cmd = ["podman", "events", "--format", "json"]
|
||||||
|
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True)
|
||||||
|
|
||||||
|
# Use select/poll for non-blocking read so we can do periodic syncs
|
||||||
|
import select
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Check if it's time for periodic sync
|
||||||
|
if time.time() - last_full_sync >= SYNC_INTERVAL:
|
||||||
|
do_full_sync()
|
||||||
|
last_full_sync = time.time()
|
||||||
|
|
||||||
|
# Check for events with timeout
|
||||||
|
ready, _, _ = select.select([process.stdout], [], [], 5)
|
||||||
|
|
||||||
|
if ready:
|
||||||
|
line = process.stdout.readline()
|
||||||
|
if line:
|
||||||
|
try:
|
||||||
|
event = json.loads(line.strip())
|
||||||
|
if event["Type"] == "container":
|
||||||
|
self.process_container_event(event)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(
|
||||||
|
f"[EVENT-ERROR] JSON decode error: {e} - Line: {line[:100]}"
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
except KeyError as e:
|
||||||
|
print(
|
||||||
|
f"[EVENT-ERROR] Missing key {e} in event: {json.dumps(event)}"
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EVENT-ERROR] Error processing event: {e}")
|
||||||
|
print(f"[EVENT-ERROR] Event structure: {json.dumps(event)}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.makedirs(CERT_DIR, exist_ok=True)
|
||||||
|
sync = PodmanHAProxyACMESync()
|
||||||
|
sync.watch_events()
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
[project]
|
||||||
|
name = "podman-haproxy-acme-sync"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"requests>=2.32.5",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2025.10.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.11"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "podman-haproxy-acme-sync"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [{ name = "requests", specifier = ">=2.32.5" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||||
|
]
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"ignition":{"config":{"merge":[{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/3yUSXOrPBaG9/0rXPSS3GAGYztdWWBjDB7ANgYP11kwKFgMAiQx3sp/73Jy892u6u5PK9WRzvtIi/P8YmCEIIU5Yl5+MTXA5HPLiM/i85D5eGIIzbEXgcfpO0wBYV5+/mLyGuAGQwqYF4or8MQUHr0zLwwHaMDdc0KRlwHmiQlyRAGi5NEd5FmBAfmdzzwxJK9wAJgXJvSo9/KEPRTcAf5BAK4BfqCzPATMiyQMP55+/UXIC8oRQKvix++GZ3L/W1LUw+K/aP/yPQJk6UmXiKF8rTkntY5z4WJzNmOnMnsZ24al3/tjOZwbzmiurPmOvzqLDVRCJT2Zu2VZWf45ycbuSdYlNqCmKEgzZRdc+3Y64Sfb0XjFXfnyrM3NIhkHjUATU+nGmyYjxkSulmGsKWpxMZYHI4uJjXOH22vlpfRFq8N0eT3w2zuPUt2Xg8Zl60glW6VS71RgzyInDtmdWN7FMku2QzPQujJnx9OjogZNv9knsyXQi0vdHBZ7edLms70SQn8hjueBr62kpNfrrXtORlGzmd75M9VqmztYw5O7GnE2Ki5DfYIFyBv5PHCUJZTnm50+V+1EUwgIE0u1bDUGvEuldkbml0OtB2ZAF7IBeMhufPtwXa5y8eB72VpTRYk7b018nY/aqJ8kE6ehgtSs2ykaxWdfkqzVDmzMgPNTb1dzvdyfyynweblM3e15V0btbhparXRtI26RGYvYVKIrPQrrPRtMGspfZ6fuYDj2ZqoUZLZbG/OjVLOSqvFCbisndg1oOIdKUxiCHg0TV7wsN++p3CfvBDeSPJ2dwg5v9TtXHZX+qrC2ELvD3shYB2K8H7u8qIjmfnU3VaObrVtz7Prt1pbZDUicrJrYCSWwEyxnLkYabyy4dJeQYaYhwaoiekWadZTfk7hTfZtVCnkWFxlSx0m4jFfwkEqdLpVr0evlepSnOGi2F6HqRKkeZnspuFh3aX0iuTAyY+3i9mWhw4nu4H10mOq86rjrMBaXESD1rtxhtGKz9h7G8tHK5mbhWYagrkzPbFngofG6PFZhvPAuRbr2j5mTmvGcivvx2nPczt4eEt5ojqHRuXRhtNkUQgSupVFMLtNOvO96lF4kZzIzLEffrXir61gMJ3vNPXPD7pxsxhNlq5prNpoFkcBlSLtqYnnetJC+L7rt4VqMJvzQ1rKTUPtVr1TueL7tjlVcXuVN3WwVZcdxdZCY/YJoiqK8/sf4T8WPt4eHOkJBFj7mu0KQfnnoz9QzPx0E6dsNqYAEGBYPo70evgwxsB++uCHlnQL8igBtcpz8yFEKEXimHo4AvaF/DpYAAQyDQZCj8NOIA5oPGg/SwXuOBwDSR1QikkGOBzgBwndgIpLnh7RgAD7rvw32XbuhAygriAH5f+gb+ml/3X27oWNXgNccAXLP6Q0tWhDY1MP09X+K75GdefDrIYsW0tcOkM9AAxHqpenbDZ08REE4616zKqXwR0UA/ovMPDEAeX4Kwm+Rf4r7hfkjY1oV3z9hPt4+Pv7x7wAAAP//hVXhITIGAAA="}]},"version":"3.3.0"}}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
variant: flatcar
|
|
||||||
version: 1.0.0
|
|
||||||
ignition:
|
|
||||||
config:
|
|
||||||
merge:
|
|
||||||
- local: rancher.ign
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"ignition":{"config":{"merge":[{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/5yRwWvbMBjF7/szPsjNsRU5DKZdNlqy1h1ha9akyehBlr7aworkSZ8NJvh/H26yQ05jO4gn0OPpPX4nMJUzZLwDcYIeQ3y7Qp7mKYMxgUg+yAqn11djMYL4eYJWUg0CMiSV1T6Sk0eEBJR3hI7iZFb+2AaMlzhIIPouKAQBWpIUifMap/zjpGLJ2Zhc58YhKrKpzjib/+oM0lx5F73FVHn3+o+/NRgc2rQNxlEz42yW3844y68O+/y3PoRHfdEsYjDSziskGj4RDRuWRgy9UZjqzPcYgtH/U/VjKSO+Xya7xdoqd2j3fKtvmu3ysFv/0F9Wg75lD4/Ptd0v1mz/XLBvG9YrXrSl+VAf+Jbpu6b6ull0JV/Hw2413N8s6vu7oi/z71Xx1FTF6nH11LCH660vE+nzwKlj5wydSaOTpUUNgkKHCbyBFn+s8wujy3AYX8bx3e8AAAD//zEhu5dSAgAA"},{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/3yTX4+qOBjG7/dTbHqrYx1xFN14geAI6qgM+Hf0osIrINByaEFh4nffoJs9m5zs6U2bvM3za9Lf840CjwYiYBT1v1EOKX8ckdSQGk10ryMuWEo8qKbnIAKO+l/fKCHCR32EQTjYZ1xQEgOqI4dRAVTw6rLD4iQF/k8cqiPOstQB1EcuEaRfDyX+QpkLFSOu9n671bzXf2azRGAOIkteQok3uP/bfK8Mkl8Yf50Ih067rre5oTyXil+LpV3LiTlUx13Mk2269qDn8cmJ6JFfTGGoRu/XXDBLPZWOyVt2z993rsY596xOR5c1b6kV8ocsd9qbJN9OxGSnC8eocdcznam3nxeyxkK51vLUbfvNtCcbPUqDteJIeGRos9t76yObzNc7p7YqdeHjJZba7qUADW+TfJm9tsr9LMmnukrbqbnIF7zV1OZrg/e2b3Iod5h7s8m66Aznbr7KdY73k31oNPVc3qldlVrSzf/RwfjiD93a9L03uwhrs13V5HyRkUXMa6fy05BNNXu9psR8N2fnHR4m+6X15qm37jTVFqPTYrqrObOe8mk7/t4uwV1Z+twfDyc0CF8zNp6N4wyS7oWMcUsv7bJ7Okcst5vbqb4aFZvQ8Fs9aX06q0P+oZUbM26H2tXHjGXedfnpj3BJw0tPMxUFY+z/6I5U5o0URRkM/qNCT7ofK/kKLiB2q//OaCCe8v20AH2taCCOB6oBd9IgqTQeTCX+p1V5c6DKWUA6oCCuLA1fGI0CCg1BUg/EgW4IFfz/hgf6ZUGaBw4cD9QuEhgwCtxn4kBHN3AsQVIx+EXRA/2EmARP7ugWiEEB/BFmUC5IFB2fWHCHxSDOIhG8ZBzSf6mojoCSUwQu6os0gzp6FKuPqrI8QA3+fBW6H+/3P/4OAAD//2a0KAm9AwAA"}]},"version":"3.3.0"}}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
variant: flatcar
|
|
||||||
version: 1.0.0
|
|
||||||
ignition:
|
|
||||||
config:
|
|
||||||
merge:
|
|
||||||
- local: base.ign
|
|
||||||
- local: k3s.ign
|
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
variable "hcloud_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "hdns_token" {
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_public_key_path" {
|
||||||
|
description = "Path to SSH public key"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ssh_private_key_path" {
|
||||||
|
description = "Path to SSH private key"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server" {
|
||||||
|
default = "mars"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "server_domain" {
|
||||||
|
default = "mars.fourlights.dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ghcr_username" {}
|
||||||
|
variable "ghcr_token" {}
|
||||||
|
|
||||||
|
resource "null_resource" "is_up" {
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = var.server
|
||||||
|
user = "fourlights"
|
||||||
|
timeout = "10m"
|
||||||
|
agent = true
|
||||||
|
agent_identity = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = [
|
||||||
|
"whoami",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "containers-network" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/network"
|
||||||
|
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
name = "containers"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "minio" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/minio"
|
||||||
|
server_ip = var.server
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
https = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "valkey" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/valkey"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "affine" {
|
||||||
|
wait_on = module.postgres.installed
|
||||||
|
source = "../../quadlets/modules/affine"
|
||||||
|
server_ip = var.server
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
# Pass postgres password for database connection
|
||||||
|
postgres_password = module.postgres.password
|
||||||
|
}
|
||||||
|
|
||||||
|
module "oci-proxy" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/oci-proxy"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "arcane" {
|
||||||
|
wait_on = module.oci-proxy.installed
|
||||||
|
source = "../../quadlets/modules/arcane"
|
||||||
|
server_domain = var.server_domain
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "qdrant" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/qdrant"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
server_domain = var.server_domain
|
||||||
|
}
|
||||||
|
|
||||||
|
module "documentdb" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/documentdb"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
#module "opensign" {
|
||||||
|
# wait_on = module.documentdb.installed
|
||||||
|
# source = "../../quadlets/modules/opensign"
|
||||||
|
# server_ip = var.server
|
||||||
|
# server_domain = var.server_domain
|
||||||
|
# ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
#}
|
||||||
|
|
||||||
|
module "postgres" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/postgres"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "rabbitmq" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/rabbitmq"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
#module "plane" {
|
||||||
|
# count = 0
|
||||||
|
# wait_on = null_resource.is_up
|
||||||
|
# source = "../../quadlets/modules/plane"
|
||||||
|
# server_ip = var.server
|
||||||
|
# ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
# server_domain = var.server_domain
|
||||||
|
#}
|
||||||
|
|
||||||
|
#module "airsonic-advanced" {
|
||||||
|
# wait_on = null_resource.is_up
|
||||||
|
# source = "../../quadlets/modules/airsonic-advanced"
|
||||||
|
# server_ip = var.server
|
||||||
|
# ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
# server_domain = var.server_domain
|
||||||
|
#}
|
||||||
|
|
||||||
|
module "gonic" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/gonic"
|
||||||
|
server_ip = var.server
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "tmail-web" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/tmail-web"
|
||||||
|
server_ip = var.server
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
server_url = "https://mail.binarysunset.dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
module "navidrome" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/navidrome"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
server_domain = var.server_domain
|
||||||
|
}
|
||||||
|
|
||||||
|
module "mopidy" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/mopidy"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
server_domain = var.server_domain
|
||||||
|
}
|
||||||
|
|
||||||
|
module "zot" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/zot"
|
||||||
|
server_ip = var.server
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
server_domain = var.server_domain
|
||||||
|
}
|
||||||
|
|
||||||
|
# Deploy Plane after all dependencies are ready
|
||||||
|
#module "plane" {
|
||||||
|
# wait_on = null_resource.is_up
|
||||||
|
#
|
||||||
|
# source = "../../quadlets/modules/plane"
|
||||||
|
# server_ip = var.server
|
||||||
|
# server_domain = var.server_domain
|
||||||
|
# ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
#
|
||||||
|
# # Pass credentials from existing services
|
||||||
|
# postgres_password = module.postgres.password
|
||||||
|
# minio_server = module.minio.server
|
||||||
|
# minio_access_key = module.minio.access_key
|
||||||
|
# minio_secret_key = module.minio.secret_key
|
||||||
|
# rabbitmq_username = module.rabbitmq.username
|
||||||
|
# rabbitmq_password = module.rabbitmq.password
|
||||||
|
#}
|
||||||
|
|
||||||
|
module "beets" {
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/beets"
|
||||||
|
server_ip = var.server
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
module "deeptutor" {
|
||||||
|
count = 0
|
||||||
|
wait_on = null_resource.is_up
|
||||||
|
source = "../../quadlets/modules/deeptutor"
|
||||||
|
server_ip = var.server
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
}
|
||||||
|
|
||||||
|
output "psql_pw" {
|
||||||
|
value = module.postgres.password
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
module "forgejo" {
|
||||||
|
wait_on = module.postgres.installed
|
||||||
|
source = "../../quadlets/modules/forgejo"
|
||||||
|
server_ip = var.server
|
||||||
|
server_domain = var.server_domain
|
||||||
|
ssh_private_key_path = var.ssh_private_key_path
|
||||||
|
|
||||||
|
# Pass postgres password for database connection
|
||||||
|
postgres_password = module.postgres.password
|
||||||
|
}
|
||||||
|
|
||||||
|
output "minio_app_urls" {
|
||||||
|
value = module.minio.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "qdrant_api_key" {
|
||||||
|
value = module.qdrant.api_key
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
#output "plane_app_urls" {
|
||||||
|
# value = module.plane.app_urls
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#output "plane_credentials" {
|
||||||
|
# value = module.plane.credentials
|
||||||
|
# sensitive = true
|
||||||
|
#}
|
||||||
|
#
|
||||||
|
#output "plane_main_url" {
|
||||||
|
# value = module.plane.main_url
|
||||||
|
#}
|
||||||
|
|
||||||
|
output "forgejo_app_urls" {
|
||||||
|
value = module.forgejo.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
output "affine_app_urls" {
|
||||||
|
value = module.affine.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
#output "deeptutor_app_urls" {
|
||||||
|
# value = module.deeptutor.app_urls
|
||||||
|
#}
|
||||||
|
|
||||||
|
output "tmail_web_app_urls" {
|
||||||
|
value = module.tmail-web.app_urls
|
||||||
|
}
|
||||||
|
|
||||||
|
#output "plane_credentials" {
|
||||||
|
#value = module.plane.credentials
|
||||||
|
#sensitive = true
|
||||||
|
#}
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"ignition":{"version":"3.3.0"},"storage":{"files":[{"path":"/etc/hostname","contents":{"compression":"","source":"data:,node"},"mode":420},{"path":"/etc/sysctl.d/20-quiet-console.conf","contents":{"compression":"","source":"data:,kernel.printk%20%3D%203%203%203%203%0A"},"mode":420},{"path":"/etc/systemd/system/serial-getty@ttyS0.service.d/override.conf","contents":{"compression":"","source":"data:;base64,W1NlcnZpY2VdCkV4ZWNTdGFydD0KRXhlY1N0YXJ0PS0vc2Jpbi9hZ2V0dHkgLS1ub2NsZWFyIC1hIHJvb3QgJUkgJFRFUk0K"},"mode":420}]},"systemd":{"units":[{"enabled":true,"name":"systemd-sysctl.service"}]}}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
# base.yaml
|
|
||||||
variant: flatcar
|
|
||||||
version: 1.0.0
|
|
||||||
storage:
|
|
||||||
files:
|
|
||||||
- path: /etc/hostname
|
|
||||||
mode: 0644
|
|
||||||
contents:
|
|
||||||
inline: node
|
|
||||||
replace: true
|
|
||||||
- path: /etc/sysctl.d/20-quiet-console.conf
|
|
||||||
mode: 0644
|
|
||||||
contents:
|
|
||||||
inline: |
|
|
||||||
kernel.printk = 3 3 3 3
|
|
||||||
- path: /etc/systemd/system/serial-getty@ttyS0.service.d/override.conf
|
|
||||||
mode: 0644
|
|
||||||
contents:
|
|
||||||
inline: |
|
|
||||||
[Service]
|
|
||||||
ExecStart=
|
|
||||||
ExecStart=-/sbin/agetty --noclear -a root %I $TERM
|
|
||||||
systemd:
|
|
||||||
units:
|
|
||||||
- name: systemd-sysctl.service
|
|
||||||
enabled: true
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"ignition":{"version":"3.3.0"},"storage":{"files":[{"path":"/etc/hostname","contents":{"compression":"","source":"data:,k3s-node"},"mode":420},{"path":"/opt/setup-k3s.sh","contents":{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/1yPT+vaQBCG7/spXrVge9gsJbaHlhyKeBClFwvtoSCbzcQs2T9hZ6wIfvgS66H8DgPDy8M8864WpvXJtJYHtcI+sdgQcKgZNy8Dok8+2gCX45QTJWHlriVAc3/EIDLxF2MuJNVYc+UzHth/P/34djyeD/XpvPu12zZLpvKHCn4rQOvOs20DNVIs9X58k86odxTaVy6BNdvUvHs/ZJZkI0Hv8YC7CnS3xhq6//jhBd+KF9LjtSWXU+8vOuaOms+bzRI8QCu1wraQFQLfY/BpZPS5gCx7KrDOEbOKY+cL9ARTchZTzedUSHNhGBJnik1uoGLGmuep7jaG/2HzTz7bflovT0XKHUEyWkIh293VbfCBsMDzWQm4kDwh/oougwPRhE/znkj9DQAA///hq7ECogEAAA=="},"mode":493}]},"systemd":{"units":[{"contents":"[Unit]\nDescription=K3s Setup\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nExecStart=/opt/setup-k3s.sh\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target\n","enabled":true,"name":"k3s-setup.service"}]}}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
variant: flatcar
|
|
||||||
version: 1.0.0
|
|
||||||
storage:
|
|
||||||
files:
|
|
||||||
- path: /etc/hostname
|
|
||||||
mode: 0644
|
|
||||||
contents:
|
|
||||||
inline: k3s-node
|
|
||||||
replace: true
|
|
||||||
- path: /opt/setup-k3s.sh
|
|
||||||
mode: 0755
|
|
||||||
contents:
|
|
||||||
inline: |
|
|
||||||
#!/bin/bash
|
|
||||||
# Install K3s with minimal components
|
|
||||||
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
|
|
||||||
--disable=traefik \
|
|
||||||
--disable=servicelb \
|
|
||||||
--tls-san=$(hostname -I | cut -d' ' -f1) \
|
|
||||||
--write-kubeconfig-mode=644" sh -
|
|
||||||
|
|
||||||
# Create symlinks for easier access
|
|
||||||
mkdir -p /root/.kube
|
|
||||||
ln -sf /etc/rancher/k3s/k3s.yaml /root/.kube/config
|
|
||||||
|
|
||||||
# Wait for node to be ready
|
|
||||||
while ! kubectl get nodes; do sleep 5; done
|
|
||||||
|
|
||||||
systemd:
|
|
||||||
units:
|
|
||||||
- name: k3s-setup.service
|
|
||||||
enabled: true
|
|
||||||
contents: |
|
|
||||||
[Unit]
|
|
||||||
Description=K3s Setup
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/opt/setup-k3s.sh
|
|
||||||
RemainAfterExit=yes
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"ignition":{"version":"3.3.0"},"storage":{"files":[{"overwrite":true,"path":"/etc/hostname","contents":{"compression":"","source":"data:,rancher-server"},"mode":420},{"path":"/opt/setup-rancher.sh","contents":{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/4xUUY/jNBB+96+Y7SIOHhzTq0CIU5CAK1y1ZUELiAdAlWNPGquObXkm7VW6H4+ctN324BAPcZzx9818M57J/Z1qXFCNpk7cw2tkNAy7LwmsI86uGdjFADpYIGRImjsSroU/QFqYqb3OyrtGZR1Mh1nlHb6cwV+vgDsMAuDht2+X3/30+P3qh3qmkM0NcFyqo+79TADcwzLQkBGeHpYvwREQ68xoBQAdibE37CcbFJ4kzHvMVXk5gwL9h1XtFvS/RO0WVJ5/SnpY0H8r2i1IoCcUAGi6CLPHCDSkFAsedkODOSDje1Vt4xBsCYRvHcNctE6Ie1i+LbSRZGJo3RbamKFD34/XMNrZC5xgz8k8Uwt24wKx9n5jXb44OJPeLNc/blaPv/z6zXq9eb16qlVMXPqgxP9dOx4Zxg/EmIEjNAgZtT2KQ+c8wt1ZBWyRIUSL9ApsBPKICT4v+4DF12oSAW+KetdCiAwpI2Hg0kV3YGLfl6zkfsrw469BWdyrMHh/uTAzZA+S2jV0zIm+UirrQ7V13A3NQJhNDIyBKxN7VbxMS6+LekUmu8Sktsiy2OUC3gF1IE/lPks0mFn2OugtZnFOT6fkjyDbS+Ap6BjpmnD7kdGjJiRl4yH4qK3az6v5olrcwMY2uy34v0mQ4cYOh4KWso25NjFYVzqpHi8HUrQgR9H1DUVKdj3GgesvPqPrnJ+mxhdj6TOmCNpaOI2DJNaNx+ean7KqTudjEaYplKbTmUlNjCt3Q7KaUVySMRk1IwTdIyVtEIxm9iineZqIp8Y9y3hPznlY4U8BIOUHPJ1Oyy+ri8QFVX/0yXkL78AMDNK+gBcg2/mnFZF3qXLxitfEyMRZp5810SFmW2vbuzAuV7CMyTujqZ6LvwMAAP//vckNzEsFAAA="},"mode":493}]},"systemd":{"units":[{"contents":"[Unit]\nDescription=Rancher Setup\nAfter=network-online.target\n# Generic condition to wait for either k3s or rke2\nAfter=k3s.service rke2-server.service\nRequires=network-online.target\n\n[Service]\nType=oneshot\nExecStart=/opt/setup-rancher.sh\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target\n","enabled":true,"name":"rancher-setup.service"}]}}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
# rancher-overlay.yaml - Universal Rancher overlay
|
|
||||||
variant: flatcar
|
|
||||||
version: 1.0.0
|
|
||||||
storage:
|
|
||||||
files:
|
|
||||||
- path: /etc/hostname
|
|
||||||
mode: 0644
|
|
||||||
overwrite: true
|
|
||||||
contents:
|
|
||||||
inline: rancher-server
|
|
||||||
- path: /opt/setup-rancher.sh
|
|
||||||
mode: 0755
|
|
||||||
contents:
|
|
||||||
inline: |
|
|
||||||
#!/bin/bash
|
|
||||||
# Detect k8s distribution and set paths
|
|
||||||
if [ -d "/var/lib/rancher/rke2" ]; then
|
|
||||||
KUBECONFIG="/etc/rancher/rke2/rke2.yaml"
|
|
||||||
# Ensure RKE2 is started
|
|
||||||
systemctl start rke2-server.service
|
|
||||||
elif [ -d "/var/lib/rancher/k3s" ]; then
|
|
||||||
KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
|
|
||||||
# Ensure K3s is started
|
|
||||||
systemctl start k3s
|
|
||||||
else
|
|
||||||
echo "No supported kubernetes distribution found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Export kubeconfig for helm and kubectl
|
|
||||||
export KUBECONFIG
|
|
||||||
# Export helm_install_dir for helm
|
|
||||||
export HELM_INSTALL_DIR=/opt/bin
|
|
||||||
|
|
||||||
# Wait for cluster to be ready
|
|
||||||
while ! kubectl get nodes; do sleep 5; done
|
|
||||||
|
|
||||||
# Install Helm if not present
|
|
||||||
if ! command -v helm &> /dev/null; then
|
|
||||||
curl -sfL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | sh -
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install cert-manager
|
|
||||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml
|
|
||||||
|
|
||||||
# Wait for cert-manager
|
|
||||||
kubectl -n cert-manager wait --for=condition=ready pod -l app=cert-manager --timeout=60s
|
|
||||||
|
|
||||||
# Install Rancher
|
|
||||||
helm repo add rancher-stable https://releases.rancher.com/server-charts/stable
|
|
||||||
helm repo update
|
|
||||||
|
|
||||||
kubectl create namespace cattle-system
|
|
||||||
helm install rancher rancher-stable/rancher \
|
|
||||||
--namespace cattle-system \
|
|
||||||
--set hostname=$(hostname | cut -d' ' -f1).sslip.io \
|
|
||||||
--set bootstrapPassword=adminadmin \
|
|
||||||
--set replicas=1
|
|
||||||
|
|
||||||
systemd:
|
|
||||||
units:
|
|
||||||
- name: rancher-setup.service
|
|
||||||
enabled: true
|
|
||||||
contents: |
|
|
||||||
[Unit]
|
|
||||||
Description=Rancher Setup
|
|
||||||
After=network-online.target
|
|
||||||
# Generic condition to wait for either k3s or rke2
|
|
||||||
After=k3s.service rke2-server.service
|
|
||||||
Requires=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/opt/setup-rancher.sh
|
|
||||||
RemainAfterExit=yes
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"ignition":{"version":"3.3.0"},"storage":{"files":[{"path":"/etc/hostname","contents":{"compression":"","source":"data:,rke2-node"},"mode":420},{"path":"/etc/rancher/rke2/config.yaml","contents":{"compression":"","source":"data:;base64,dGxzLXNhbjoKICAtICQoaG9zdG5hbWUgLUkgfCBjdXQgLWQnICcgLWYxKQp3cml0ZS1rdWJlY29uZmlnLW1vZGU6ICIwNjQ0Igo="},"mode":420},{"path":"/opt/setup-rke2.sh","contents":{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/4SQsWrDQAyG93uKv3i2BRm7lg6lnfoG54scH5bvgiQHDH34ckkLaZdMAknf9wt1TzTmQmO0OXR4K+ZRBJ/vr4eQNhX0Nn1gdj/bM9GJfdCFD0Ou+ILN6IPt5rwmF3CJozDavDfWC+vQSk4cQocX5egM21fJZTFMVbFsIzcyliOS5uQSpKA30CUqSR5JY0kzKzXp9c5fhDZTkpqi3Lcf4reU//RPdliXY1b0Z5DW6jQ06005gdjTX9/1EXtc5X6dUi1TPoXvAAAA///HYfmJVwEAAA=="},"mode":493}]},"systemd":{"units":[{"contents":"[Unit]\nDescription=RKE2 Setup\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nExecStart=/opt/setup-rke2.sh\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target\n","enabled":true,"name":"rke2-setup.service"}]}}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
# rke2-base-config.yaml - Base Flatcar + RKE2 setup
|
|
||||||
variant: flatcar
|
|
||||||
version: 1.0.0
|
|
||||||
storage:
|
|
||||||
files:
|
|
||||||
- path: /etc/hostname
|
|
||||||
mode: 0644
|
|
||||||
contents:
|
|
||||||
inline: rke2-node
|
|
||||||
- path: /etc/rancher/rke2/config.yaml
|
|
||||||
mode: 0644
|
|
||||||
contents:
|
|
||||||
inline: |
|
|
||||||
tls-san:
|
|
||||||
- $(hostname -I | cut -d' ' -f1)
|
|
||||||
write-kubeconfig-mode: "0644"
|
|
||||||
- path: /opt/setup-rke2.sh
|
|
||||||
mode: 0755
|
|
||||||
contents:
|
|
||||||
inline: |
|
|
||||||
#!/bin/bash
|
|
||||||
# Install RKE2
|
|
||||||
curl -sfL https://get.rke2.io | sh -
|
|
||||||
systemctl enable rke2-server.service
|
|
||||||
|
|
||||||
# Create symlinks for kubectl and crictl
|
|
||||||
ln -s /var/lib/rancher/rke2/bin/kubectl /usr/local/bin/kubectl
|
|
||||||
ln -s /var/lib/rancher/rke2/bin/crictl /usr/local/bin/crictl
|
|
||||||
|
|
||||||
mkdir -p /root/.kube
|
|
||||||
ln -sf /etc/rancher/rke2/rke2.yaml /root/.kube/config
|
|
||||||
|
|
||||||
systemd:
|
|
||||||
units:
|
|
||||||
- name: rke2-setup.service
|
|
||||||
enabled: true
|
|
||||||
contents: |
|
|
||||||
[Unit]
|
|
||||||
Description=RKE2 Setup
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=/opt/setup-rke2.sh
|
|
||||||
RemainAfterExit=yes
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"ignition":{"config":{"merge":[{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/5yRwWvbMBjF7/szPsjNsRU5DKZdNlqy1h1ha9akyehBlr7aworkSZ8NJvh/H26yQ05jO4gn0OPpPX4nMJUzZLwDcYIeQ3y7Qp7mKYMxgUg+yAqn11djMYL4eYJWUg0CMiSV1T6Sk0eEBJR3hI7iZFb+2AaMlzhIIPouKAQBWpIUifMap/zjpGLJ2Zhc58YhKrKpzjib/+oM0lx5F73FVHn3+o+/NRgc2rQNxlEz42yW3844y68O+/y3PoRHfdEsYjDSziskGj4RDRuWRgy9UZjqzPcYgtH/U/VjKSO+Xya7xdoqd2j3fKtvmu3ysFv/0F9Wg75lD4/Ptd0v1mz/XLBvG9YrXrSl+VAf+Jbpu6b6ull0JV/Hw2413N8s6vu7oi/z71Xx1FTF6nH11LCH660vE+nzwKlj5wydSaOTpUUNgkKHCbyBFn+s8wujy3AYX8bx3e8AAAD//zEhu5dSAgAA"},{"compression":"gzip","source":"data:;base64,H4sIAAAAAAAC/5STT2/yOBDG7/spVr6WkkASVrDiQAOFAKUNgTeBwsE4k8QksVPbCYWK774KrPaPVl3p9cWWZjQ/PzPzfCEaM6ooZ6j3hSoQ8vZERtNo6ujaQFJxgWOooxHNQKLe+xcqsEpQD2mgiJZwqRjOATUQ4UwBU7JOJjwvBMg/y6EGkrwUBFAPhVjhXkOk0H5kPIQaktd3z2zr18a/iwvMSAJCq7M1wllE4+YZ59nPwX4/YAkdsxGOPy/zYJEcjnzm2APl2C7H4+4lHFvJwV/H83UaR/bTMQzceO67zLFJPPc3nzO3MEie6VuvJUJ/mm3a3XKbZ2zut6rteN1xbOe0OLq6E/P+t3J4oTQJqiweazFNmfyvhvhCi291TEzpDO7H1kzPlb4YuoPzuGuUs8qg7adl/lsWdzIW8bFlSjKxDlVM3clwYpgkTed4G74MUhZ1T6q1uqxyN3/VX4OlOXvYLp+mWiVMcBdJoC9YKw6P2uGlO55Gof081F/LB2e+6DhvyhKn/Nmwp/wyxNUw8u2HyvVSk7icBBbEL+1WNN2unl9+HJ6lcyHnjL56Vul6xWxg6Pb6w1ta042eXmaXoOgSK0qG21VafFBjTiJTwFrTlm9hRoNN66BvraHfObp6R9etODyugq7WGgWKWEEnXNPW6o0HVd0MTdMmmyif/jiNBoNB/5+z6BrXfb3NZ6kgD+t+l4yq+zb/PQX0vmZU7XdsCJIIWtS+6C9no/avXj25HRtECkSfgTpxkT5yllEGTYVFDGrHfMyU/C64Y+8eiIoS2O/Y6lxAnzOQCVc7NvoE4iksVP+/S7JjS8gxvYNHn1T1zyBv1RwmFc6y/Z0L4dO5n5eZoo+lBPEXFjUQMHzIIEQ9JUpooJtXe+jmvxuqKe//Qtf99frLHwEAAP//D4wVlBEEAAA="}]},"version":"3.3.0"}}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
variant: flatcar
|
|
||||||
version: 1.0.0
|
|
||||||
ignition:
|
|
||||||
config:
|
|
||||||
merge:
|
|
||||||
- local: base.ign
|
|
||||||
- local: rke2.ign
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
TARGET=${1:-"k3s"}
|
|
||||||
CPU=${2:-2048}
|
|
||||||
|
|
||||||
echo "Booting $TARGET"
|
|
||||||
qemu-system-x86_64 \
|
|
||||||
-m $CPU \
|
|
||||||
-cpu host \
|
|
||||||
-enable-kvm \
|
|
||||||
-smp $(nproc) \
|
|
||||||
-drive if=pflash,format=raw,readonly=on,file=/usr/share/edk2/x64/OVMF_CODE.4m.fd \
|
|
||||||
-drive if=pflash,format=raw,file=uefi/OVMF_VARS.4m.fd \
|
|
||||||
-drive if=virtio,file=disks/$TARGET.qcow2,cache=none,aio=native \
|
|
||||||
-fw_cfg name=opt/org.flatcar-linux/config,file=$TARGET.ign \
|
|
||||||
-device virtio-net-pci,netdev=net0 \
|
|
||||||
-netdev user,id=net0
|
|
||||||
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,561 @@
|
||||||
|
#cloud-config
|
||||||
|
users:
|
||||||
|
- name: fourlights
|
||||||
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||||
|
groups: users,admin,sudo
|
||||||
|
shell: /bin/bash
|
||||||
|
lock_passwd: false
|
||||||
|
ssh_authorized_keys:
|
||||||
|
- ${ssh_public_key}
|
||||||
|
|
||||||
|
packages:
|
||||||
|
- podman
|
||||||
|
- haproxy
|
||||||
|
- python3
|
||||||
|
- python3-requests
|
||||||
|
- curl
|
||||||
|
- wget
|
||||||
|
- jq
|
||||||
|
- socat
|
||||||
|
- nmap
|
||||||
|
|
||||||
|
package_update: true
|
||||||
|
package_upgrade: true
|
||||||
|
|
||||||
|
write_files:
|
||||||
|
- path: /etc/sudoers.d/fourlights-haproxy
|
||||||
|
permissions: '0440'
|
||||||
|
content: |
|
||||||
|
fourlights ALL=(root) NOPASSWD: /bin/systemctl reload haproxy
|
||||||
|
fourlights ALL=(root) NOPASSWD: /bin/systemctl restart haproxy
|
||||||
|
fourlights ALL=(root) NOPASSWD: /bin/systemctl stop haproxy
|
||||||
|
fourlights ALL=(root) NOPASSWD: /bin/systemctl start haproxy
|
||||||
|
fourlights ALL=(root) NOPASSWD: /bin/chown -R haproxy\:haproxy /etc/ssl/haproxy/*
|
||||||
|
fourlights ALL=(root) NOPASSWD: /bin/chmod 600 /etc/ssl/haproxy/*
|
||||||
|
# HAProxy main configuration
|
||||||
|
- path: /etc/haproxy/haproxy.cfg
|
||||||
|
content: |
|
||||||
|
global
|
||||||
|
daemon
|
||||||
|
stats socket /var/run/haproxy/admin.sock mode 660 level admin expose-fd listeners
|
||||||
|
stats timeout 30s
|
||||||
|
user haproxy
|
||||||
|
group haproxy
|
||||||
|
log stdout local0 info
|
||||||
|
|
||||||
|
defaults
|
||||||
|
mode http
|
||||||
|
timeout connect 5000ms
|
||||||
|
timeout client 50000ms
|
||||||
|
timeout server 50000ms
|
||||||
|
option httplog
|
||||||
|
log global
|
||||||
|
|
||||||
|
# Stats interface
|
||||||
|
frontend stats
|
||||||
|
bind *:8404
|
||||||
|
http-request use-service prometheus-exporter if { path /metrics }
|
||||||
|
stats enable
|
||||||
|
stats uri /stats
|
||||||
|
stats refresh 10s
|
||||||
|
|
||||||
|
# HTTP Frontend
|
||||||
|
frontend main
|
||||||
|
bind *:80
|
||||||
|
# ACL to detect ACME challenge requests
|
||||||
|
acl is_acme_challenge path_beg /.well-known/acme-challenge/
|
||||||
|
# Route ACME challenges to the acme_challenge backend
|
||||||
|
use_backend acme_challenge if is_acme_challenge
|
||||||
|
default_backend no_match
|
||||||
|
|
||||||
|
# HTTPS Frontend
|
||||||
|
frontend https_main
|
||||||
|
bind *:443
|
||||||
|
default_backend no_match
|
||||||
|
|
||||||
|
# ACME Challenge Backend
|
||||||
|
backend acme_challenge
|
||||||
|
mode http
|
||||||
|
server acme_server 127.0.0.1:8888
|
||||||
|
|
||||||
|
# Default backend
|
||||||
|
backend no_match
|
||||||
|
http-request return status 404 content-type text/plain string "No matching service found"
|
||||||
|
|
||||||
|
- path: /etc/dataplaneapi/dataplaneapi.yml
|
||||||
|
content: |
|
||||||
|
dataplaneapi:
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 5555
|
||||||
|
user:
|
||||||
|
- insecure: true
|
||||||
|
password: admin
|
||||||
|
username: admin
|
||||||
|
|
||||||
|
haproxy:
|
||||||
|
config_file: /etc/haproxy/haproxy.cfg
|
||||||
|
haproxy_bin: /usr/sbin/haproxy
|
||||||
|
reload:
|
||||||
|
reload_cmd: systemctl reload haproxy
|
||||||
|
restart_cmd: systemctl restart haproxy
|
||||||
|
stats_socket: /var/run/haproxy/admin.sock
|
||||||
|
|
||||||
|
- path: /usr/local/bin/podman-haproxy-acme-sync-wrapper.sh
|
||||||
|
permissions: '0755'
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
MAX_WAIT=60
|
||||||
|
ELAPSED=0
|
||||||
|
|
||||||
|
# Wait for HAProxy
|
||||||
|
echo "Checking HAProxy status..."
|
||||||
|
while ! systemctl is-active --quiet haproxy; do
|
||||||
|
echo "Waiting for HAProxy to start..."
|
||||||
|
sleep 2
|
||||||
|
ELAPSED=$($ELAPSED + 2)
|
||||||
|
[ $ELAPSED -ge $MAX_WAIT ] && { echo "ERROR: HAProxy timeout"; exit 1; }
|
||||||
|
done
|
||||||
|
echo "HAProxy is active"
|
||||||
|
|
||||||
|
# Reset and wait for Data Plane API to actually respond
|
||||||
|
ELAPSED=0
|
||||||
|
echo "Checking Data Plane API readiness..."
|
||||||
|
while true; do
|
||||||
|
HTTP_CODE=$(curl -s -w "%%{http_code}" -o /dev/null \
|
||||||
|
--connect-timeout 5 \
|
||||||
|
--max-time 10 \
|
||||||
|
-u :admin \
|
||||||
|
http://localhost:5555/v3/services/haproxy/configuration/version 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
[ "$HTTP_CODE" = "200" ] && { echo "Data Plane API ready"; break; }
|
||||||
|
|
||||||
|
echo "Waiting for Data Plane API... (HTTP $HTTP_CODE)"
|
||||||
|
sleep 2
|
||||||
|
ELAPSED=$((ELAPSED + 2))
|
||||||
|
|
||||||
|
if [ $ELAPSED -ge $MAX_WAIT ]; then
|
||||||
|
echo "ERROR: Data Plane API not ready within $MAX_WAITs (HTTP $HTTP_CODE)"
|
||||||
|
journalctl -u dataplaneapi -n 50 --no-pager
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
exec /usr/local/bin/podman-haproxy-acme-sync.py
|
||||||
|
|
||||||
|
# Podman HAProxy ACME Sync Script
|
||||||
|
- path: /usr/local/bin/podman-haproxy-acme-sync.py
|
||||||
|
permissions: '0755'
|
||||||
|
content: |
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HAPROXY_API_BASE = "http://:admin@127.0.0.1:5555/v3"
|
||||||
|
CERT_DIR = "/home/fourlights/.acme.sh"
|
||||||
|
ACME_SCRIPT = "/usr/local/bin/acme.sh"
|
||||||
|
|
||||||
|
class PodmanHAProxyACMESync:
|
||||||
|
def __init__(self):
|
||||||
|
self.ssl_services = set()
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({'Content-Type': 'application/json'})
|
||||||
|
|
||||||
|
def get_next_index(self, path):
|
||||||
|
response = self.session.get(f"{HAPROXY_API_BASE}/services/haproxy/configuration/{path}")
|
||||||
|
return len(response.json()) if response.status_code == 200 else None
|
||||||
|
|
||||||
|
def get_dataplaneapi_version(self):
|
||||||
|
response = self.session.get(f"{HAPROXY_API_BASE}/services/haproxy/configuration/version")
|
||||||
|
return response.json() if response.status_code == 200 else None
|
||||||
|
|
||||||
|
def get_container_labels(self, container_id):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['podman', 'inspect', container_id],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
data = json.loads(result.stdout)
|
||||||
|
return data[0]['Config']['Labels'] or {}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting labels for {container_id}: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def request_certificate(self, domain):
|
||||||
|
print(f"[CERT-REQUEST] About to request certificate for {domain}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cmd = [
|
||||||
|
ACME_SCRIPT,
|
||||||
|
"--issue",
|
||||||
|
"-d", domain,
|
||||||
|
"--standalone",
|
||||||
|
"--httpport", "8888",
|
||||||
|
"--server", "letsencrypt",
|
||||||
|
"--listen-v4",
|
||||||
|
"--debug", "2"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Log the command being executed
|
||||||
|
print(f"[CERT-REQUEST] Executing: {' '.join(cmd)}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
# Log both stdout and stderr for complete debugging
|
||||||
|
if result.stdout:
|
||||||
|
print(f"[CERT-STDOUT] {result.stdout}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
if result.stderr:
|
||||||
|
print(f"[CERT-STDERR] {result.stderr}")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(f"[CERT-SUCCESS] Certificate obtained for {domain}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.install_certificate(domain)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"[CERT-FAILED] Failed to obtain certificate for {domain}")
|
||||||
|
print(f"[CERT-FAILED] Return code: {result.returncode}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CERT-ERROR] Error requesting certificate: {e}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def install_certificate(self, domain):
|
||||||
|
cert_file = f"{CERT_DIR}/{domain}.pem"
|
||||||
|
|
||||||
|
try:
|
||||||
|
acme_cert_dir = f"/home/fourlights/.acme.sh/{domain}_ecc"
|
||||||
|
|
||||||
|
with open(cert_file, 'w') as outfile:
|
||||||
|
with open(f"{acme_cert_dir}/fullchain.cer") as cert:
|
||||||
|
outfile.write(cert.read())
|
||||||
|
with open(f"{acme_cert_dir}/{domain}.key") as key:
|
||||||
|
outfile.write(key.read())
|
||||||
|
try:
|
||||||
|
with open(f"{acme_cert_dir}/ca.cer") as ca:
|
||||||
|
outfile.write(ca.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
os.chmod(cert_file, 0o600)
|
||||||
|
print(f"Certificate installed at {cert_file}")
|
||||||
|
|
||||||
|
self.update_haproxy_ssl_bind(domain)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error installing certificate for {domain}: {e}")
|
||||||
|
|
||||||
|
def update_haproxy_ssl_bind(self, domain):
|
||||||
|
print(f"Updating ssl bind for {domain}")
|
||||||
|
try:
|
||||||
|
ssl_bind_data = {
|
||||||
|
"address": "*",
|
||||||
|
"port": 443,
|
||||||
|
"ssl": True,
|
||||||
|
"ssl_certificate": f"{CERT_DIR}/{domain}.pem",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/https_main/binds?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=ssl_bind_data)
|
||||||
|
print(response.json())
|
||||||
|
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
print(f"Updated HAProxy SSL bind for {domain}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating HAProxy SSL bind: {e}")
|
||||||
|
|
||||||
|
def setup_certificate_renewal(self, domain):
|
||||||
|
renewal_script = f"/etc/cron.d/acme-{domain.replace('.', '-')}"
|
||||||
|
|
||||||
|
cron_content = f"""0 0 * * * root {ACME_SCRIPT} --renew -d {domain} --post-hook "systemctl reload haproxy" >/dev/null 2>&1
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(renewal_script, 'w') as f:
|
||||||
|
f.write(cron_content)
|
||||||
|
|
||||||
|
print(f"Setup automatic renewal for {domain}")
|
||||||
|
|
||||||
|
def update_haproxy_backend(self, service_name, host, port, action='add'):
|
||||||
|
backend_name = f"backend_{service_name}"
|
||||||
|
server_name = f"{service_name}_server"
|
||||||
|
|
||||||
|
if action == 'add':
|
||||||
|
backend_data = {
|
||||||
|
"name": backend_name,
|
||||||
|
"mode": "http",
|
||||||
|
"balance": {"algorithm": "roundrobin"},
|
||||||
|
}
|
||||||
|
backends = self.session.post(f"{HAPROXY_API_BASE}/services/haproxy/configuration/backends?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=backend_data)
|
||||||
|
print(backends.json())
|
||||||
|
|
||||||
|
server_data = {
|
||||||
|
"name": server_name,
|
||||||
|
"address": host,
|
||||||
|
"port": int(port),
|
||||||
|
"check": "enabled",
|
||||||
|
}
|
||||||
|
tweak = self.session.post(f"{HAPROXY_API_BASE}/services/haproxy/configuration/backends/{backend_name}/servers?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=server_data)
|
||||||
|
print(tweak.json())
|
||||||
|
|
||||||
|
elif action == 'remove':
|
||||||
|
self.session.delete(f"{HAPROXY_API_BASE}/services/haproxy/configuration/backends/{backend_name}/servers/{server_name}?version={self.get_dataplaneapi_version()}")
|
||||||
|
|
||||||
|
def update_haproxy_frontend_rule(self, service_name, domain, ssl_enabled=False, action='add'):
|
||||||
|
if action == 'add':
|
||||||
|
if ssl_enabled and domain and domain not in self.ssl_services:
|
||||||
|
print(f"Setting up SSL for {domain}")
|
||||||
|
if self.request_certificate(domain):
|
||||||
|
self.setup_certificate_renewal(domain)
|
||||||
|
self.ssl_services.add(domain)
|
||||||
|
|
||||||
|
acl_data = {
|
||||||
|
"acl_name": f"is_{service_name}",
|
||||||
|
"criterion": "hdr(host)",
|
||||||
|
"value": domain,
|
||||||
|
}
|
||||||
|
self.session.post(f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/main/acls/{self.get_next_index('frontends/main/acls')}?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=acl_data)
|
||||||
|
|
||||||
|
if ssl_enabled:
|
||||||
|
self.session.post(f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/https_main/acls/{self.get_next_index('frontends/https_main/acls')}?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=acl_data)
|
||||||
|
|
||||||
|
rule_data = {
|
||||||
|
"name": f"backend_{service_name}",
|
||||||
|
"cond": "if",
|
||||||
|
"cond_test": f"is_{service_name}",
|
||||||
|
}
|
||||||
|
self.session.post(f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/main/backend_switching_rules/{self.get_next_index('frontends/main/backend_switching_rules')}?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=rule_data)
|
||||||
|
|
||||||
|
if ssl_enabled:
|
||||||
|
self.session.post(f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/https_main/backend_switching_rules/{self.get_next_index('frontends/https_main/backend_switching_rules')}?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=rule_data)
|
||||||
|
|
||||||
|
redirect_rule = {
|
||||||
|
"type": "redirect",
|
||||||
|
"redirect_rule": {
|
||||||
|
"type": "scheme",
|
||||||
|
"value": "https",
|
||||||
|
"code": 301
|
||||||
|
},
|
||||||
|
"cond": "if",
|
||||||
|
"cond_test": f"is_{service_name}",
|
||||||
|
}
|
||||||
|
self.session.post(f"{HAPROXY_API_BASE}/services/haproxy/configuration/frontends/main/http_request_rules/{self.get_next_index('frontends/main/http_request_rules')}?version={self.get_dataplaneapi_version()}",
|
||||||
|
json=redirect_rule)
|
||||||
|
|
||||||
|
def process_container_event(self, event):
|
||||||
|
# DIAGNOSTIC: Log raw event structure
|
||||||
|
print(f"[EVENT-DEBUG] Received event - Type: {event.get('Type', 'MISSING')}, Action: {event.get('Action', 'MISSING')}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
# DIAGNOSTIC: Check for Actor key
|
||||||
|
if 'Actor' not in event:
|
||||||
|
print(f"[EVENT-SKIP] Skipping event without 'Actor' key - Full event: {json.dumps(event)}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# DIAGNOSTIC: Check for ID in Actor
|
||||||
|
if 'ID' not in event['Actor']:
|
||||||
|
print(f"[EVENT-SKIP] Skipping event without 'Actor.ID' - Actor content: {json.dumps(event['Actor'])}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
container_id = event['Actor']['ID'][:12]
|
||||||
|
action = event['Action']
|
||||||
|
|
||||||
|
print(f"[EVENT-PROCESS] Processing '{action}' event for container {container_id}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
labels = self.get_container_labels(container_id)
|
||||||
|
|
||||||
|
# Dictionary to store discovered services
|
||||||
|
services = {}
|
||||||
|
|
||||||
|
# First, check for namespaced labels (haproxy.{service_name}.enable)
|
||||||
|
for label_key, label_value in labels.items():
|
||||||
|
if label_key.startswith('haproxy.') and label_key.endswith('.enable') and label_value.lower() == 'true':
|
||||||
|
# Extract service name from label key
|
||||||
|
parts = label_key.split('.')
|
||||||
|
if len(parts) == 3: # haproxy.{service_name}.enable
|
||||||
|
service_name = parts[1]
|
||||||
|
|
||||||
|
# Extract properties for this service namespace
|
||||||
|
service_config = {
|
||||||
|
'service_name': service_name,
|
||||||
|
'host': labels.get(f'haproxy.{service_name}.host', '127.0.0.1'),
|
||||||
|
'port': labels.get(f'haproxy.{service_name}.port', '8080'),
|
||||||
|
'domain': labels.get(f'haproxy.{service_name}.domain', None),
|
||||||
|
'ssl_enabled': labels.get(f'haproxy.{service_name}.tls', 'false').lower() == 'true'
|
||||||
|
}
|
||||||
|
services[service_name] = service_config
|
||||||
|
|
||||||
|
# Backward compatibility: If no namespaced labels found, check for flat labels
|
||||||
|
if not services and 'haproxy.enable' in labels and labels['haproxy.enable'].lower() == 'true':
|
||||||
|
service_name = labels.get('haproxy.service', container_id)
|
||||||
|
services[service_name] = {
|
||||||
|
'service_name': service_name,
|
||||||
|
'host': labels.get('haproxy.host', '127.0.0.1'),
|
||||||
|
'port': labels.get('haproxy.port', '8080'),
|
||||||
|
'domain': labels.get('haproxy.domain', None),
|
||||||
|
'ssl_enabled': labels.get('haproxy.tls', 'false').lower() == 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process each discovered service
|
||||||
|
for service_name, config in services.items():
|
||||||
|
if action in ['start', 'restart']:
|
||||||
|
print(f"Adding service {config['service_name']} to HAProxy (SSL: {config['ssl_enabled']}, Domain: {config['domain']})")
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.update_haproxy_backend(config['service_name'], config['host'], config['port'], 'add')
|
||||||
|
if config['domain']:
|
||||||
|
self.update_haproxy_frontend_rule(config['service_name'], config['domain'], config['ssl_enabled'], 'add')
|
||||||
|
|
||||||
|
elif action in ['stop', 'remove', 'died']:
|
||||||
|
print(f"Removing service {config['service_name']} from HAProxy")
|
||||||
|
sys.stdout.flush()
|
||||||
|
self.update_haproxy_backend(config['service_name'], config['host'], config['port'], 'remove')
|
||||||
|
|
||||||
|
def watch_events(self):
|
||||||
|
print("Starting Podman-HAProxy-ACME sync...")
|
||||||
|
|
||||||
|
# Track last sync time
|
||||||
|
last_full_sync = 0
|
||||||
|
SYNC_INTERVAL = 60 # Re-scan all containers every 60 seconds
|
||||||
|
|
||||||
|
def do_full_sync():
|
||||||
|
"""Perform a full sync of all running containers"""
|
||||||
|
print("Performing full container sync...")
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['podman', 'ps', '--format', 'json'],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
if result.returncode == 0:
|
||||||
|
containers = json.loads(result.stdout)
|
||||||
|
for container in containers:
|
||||||
|
event = {
|
||||||
|
'Type': 'container',
|
||||||
|
'Action': 'start',
|
||||||
|
'Actor': {'ID': container.get('Id', '')}
|
||||||
|
}
|
||||||
|
self.process_container_event(event)
|
||||||
|
print(f"Synced {len(containers)} containers")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during full sync: {e}")
|
||||||
|
|
||||||
|
# Initial sync
|
||||||
|
do_full_sync()
|
||||||
|
last_full_sync = time.time()
|
||||||
|
|
||||||
|
print("Watching for container events...")
|
||||||
|
|
||||||
|
cmd = ['podman', 'events', '--format', 'json']
|
||||||
|
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True)
|
||||||
|
|
||||||
|
# Use select/poll for non-blocking read so we can do periodic syncs
|
||||||
|
import select
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Check if it's time for periodic sync
|
||||||
|
if time.time() - last_full_sync >= SYNC_INTERVAL:
|
||||||
|
do_full_sync()
|
||||||
|
last_full_sync = time.time()
|
||||||
|
|
||||||
|
# Check for events with timeout
|
||||||
|
ready, _, _ = select.select([process.stdout], [], [], 5)
|
||||||
|
|
||||||
|
if ready:
|
||||||
|
line = process.stdout.readline()
|
||||||
|
if line:
|
||||||
|
try:
|
||||||
|
event = json.loads(line.strip())
|
||||||
|
if event['Type'] == 'container':
|
||||||
|
self.process_container_event(event)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"[EVENT-ERROR] JSON decode error: {e} - Line: {line[:100]}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except KeyError as e:
|
||||||
|
print(f"[EVENT-ERROR] Missing key {e} in event: {json.dumps(event)}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[EVENT-ERROR] Error processing event: {e}")
|
||||||
|
print(f"[EVENT-ERROR] Event structure: {json.dumps(event)}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.makedirs(CERT_DIR, exist_ok=True)
|
||||||
|
sync = PodmanHAProxyACMESync()
|
||||||
|
sync.watch_events()
|
||||||
|
|
||||||
|
runcmd:
|
||||||
|
# Create necessary directories
|
||||||
|
- mkdir -p /var/run/haproxy /etc/ssl/haproxy /etc/containers/systemd /etc/haproxy/dataplane /etc/dataplaneapi
|
||||||
|
- chown haproxy:haproxy /var/run/haproxy
|
||||||
|
|
||||||
|
# Install Data Plane API
|
||||||
|
- cd /tmp && curl -LO https://github.com/haproxytech/dataplaneapi/releases/download/v3.2.4/dataplaneapi_3.2.4_linux_amd64.deb
|
||||||
|
- env DEBIAN_FRONTEND=noninteractive apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" /tmp/dataplaneapi_3.2.4_linux_amd64.deb
|
||||||
|
- rm /tmp/dataplaneapi_3.2.4_linux_amd64.deb
|
||||||
|
|
||||||
|
- mkdir -p /home/fourlights/.config/containers/systemd
|
||||||
|
- mkdir -p /home/fourlights/.config/systemd/user
|
||||||
|
- |
|
||||||
|
cat > /home/fourlights/.config/systemd/user/podman-haproxy-acme-sync.service << 'EOF'
|
||||||
|
[Unit]
|
||||||
|
Description=Podman HAProxy ACME Sync Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Environment="XDG_RUNTIME_DIR=/run/user/1000"
|
||||||
|
Environment="DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus"
|
||||||
|
ExecStart=/usr/local/bin/podman-haproxy-acme-sync-wrapper.sh
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
EOF
|
||||||
|
- chown -R fourlights:fourlights /home/fourlights
|
||||||
|
|
||||||
|
# Install ACME.sh
|
||||||
|
- su - fourlights -c 'curl https://get.acme.sh | sh -s email=${acme_email}'
|
||||||
|
- ln -sf /home/fourlights/.acme.sh/acme.sh /usr/local/bin/acme.sh
|
||||||
|
|
||||||
|
# Setup data directory and mount volume
|
||||||
|
- mkdir -p /opt/storage/data
|
||||||
|
- mkfs.ext4 -F /dev/sdb
|
||||||
|
- mount /dev/sdb /opt/storage/data
|
||||||
|
- echo '/dev/sdb /opt/storage/data ext4 defaults 0 2' >> /etc/fstab
|
||||||
|
- chown -R fourlights:fourlights /opt/storage/data
|
||||||
|
|
||||||
|
# Enable Podman for user services
|
||||||
|
- loginctl enable-linger fourlights
|
||||||
|
- su - fourlights -c 'podman login ghcr.io -u ${ghcr_username} -p ${ghcr_token}'
|
||||||
|
|
||||||
|
# Enable and start services
|
||||||
|
- systemctl daemon-reload
|
||||||
|
- systemctl enable --now haproxy
|
||||||
|
- systemctl enable --now dataplaneapi
|
||||||
|
- su - fourlights -c 'systemctl --user daemon-reload'
|
||||||
|
- su - fourlights -c 'systemctl --user enable --now podman-haproxy-acme-sync'
|
||||||
|
|
||||||
|
final_message: "Server setup complete with HAProxy, Podman, and ACME sync configured"
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
```release-note:feature
|
||||||
|
`provider`: Add a new attribute `burst_limit` for client-side throttling limit configuration.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:doc
|
||||||
|
`provider`: Add a new attribute `burst_limit`.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:doc
|
||||||
|
`resource/helm_release`: Add usage examples for `GCS` and `S3` plugins.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:doc
|
||||||
|
`data_source/helm_template`: Correct some errors in examples.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:dependency
|
||||||
|
Bump `github.com/containerd/containerd` from `1.6.6` to `1.6.12`
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:doc
|
||||||
|
`resource/helm_release`: Add usage example for `OCI` repositories.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:dependency
|
||||||
|
Bump `helm.sh/helm/v3` from `3.9.4` to `3.11.0`
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
```release-note:dependency
|
||||||
|
Bump `k8s.io/client-go` from `0.24.2` to `0.26.1`
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:note
|
||||||
|
`provider`: `kubernetes.exec.api_version` no longer supports `client.authentication.k8s.io/v1alpha1`. Please, switch to `client.authentication.k8s.io/v1beta1` or `client.authentication.k8s.io/v1`.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
```release-note:enhancement
|
||||||
|
`data_source/helm_template`: Add a new attribute `crds` which when `include_crds` is set to `true` will be populated with a list of the manifests from the `crds/` folder of the chart.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:doc
|
||||||
|
`data_source/helm_template`: Add a new attribute `crds`
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:doc
|
||||||
|
`provider`: Add a note regarding the `KUBECONFIG` environment variable.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:dependency
|
||||||
|
Bump `helm.sh/helm/v3` from `3.11.0` to `3.11.1`
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:dependency
|
||||||
|
Bump `golang.org/x/crypto` from `0.5.0` to `0.6.0`
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
`helm/resource_release.go`: Add `set_list` attribute
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`helm/resource_release.go`: Always recompute metadata when a release is updated
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:dependency
|
||||||
|
Bump `helm.sh/helm/v3` from `3.11.2` to `3.12.0`
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`helm/resource_release.go`: Fix: only recompute metadata if version actually changes
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`helm/resource_release.go`: Fix: version conflicts when using local chart
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:enhancement
|
||||||
|
`resource/helm_release`: add `name` field validation to be limited to 53 characters.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`resource/helm_release`: Add nil check for `set_list.value` to prevent provider ChartPathOptions
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
```release-note:bug
|
||||||
|
`helm_release`: Fix perpetual diff when version attribute is an empty string
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:enhancement
|
||||||
|
resource/helm_release: add `upgrade_install` boolean attribute to enable idempotent release installation, addressing components of [GH-425](https://github.com/hashicorp/terraform-provider-helm/issues/425)
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
```release-note:dependency
|
||||||
|
Bump Golang from `1.20` to `1.21`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump helm.sh/helm/v3 from `v3.13.1` to `v3.13.2`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump github.com/hashicorp/go-cty from `v1.4.1-0.20200414143053-d3edf31b6320` to `v1.4.1-0.20200723130312-85980079f637`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump github.com/hashicorp/terraform-plugin-docs from `v0.14.1` to `v0.16.0`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump github.com/hashicorp/terraform-plugin-sdk/v2 from `v2.26.1` to `v2.30.0`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump golang.org/x/crypto from `v0.14.0` to `v0.16.0`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump k8s.io/api from `v0.28.3` to `v0.28.4`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump k8s.io/apimachinery from `v0.28.3` to `v0.28.4`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump k8s.io/client-go from `v0.28.3` to `v0.28.4`.
|
||||||
|
```
|
||||||
|
|
||||||
|
```release-note:dependency
|
||||||
|
Bump sigs.k8s.io/yaml from `v1.3.0` to `v1.4.0`.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`provider`: Fix manifest diff rendering for OCI charts.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:doc
|
||||||
|
`docs`: Use templatefile() instead of "template_file" provider in GKE example.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note: enhancement
|
||||||
|
`resource/helm_release`: enable helm lockup function.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:enhancement
|
||||||
|
Add support for Terraform's experimental deferred actions
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:dependency
|
||||||
|
Provider project ported from `terraform-plugin-sdk/v2` to `terraform-plugin-framework`
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:enhancement
|
||||||
|
`helm_release`: add new attributes metadata.last_deployed, metadata.first_deployed, metadata.notes
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`resource/helm_release`: Fix: only recompute metadata when the version in the metadata changes
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`helm_release`: On destroy, do not error when release is not found
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`helm_release`: Fix nil pointer deref panic on destroy when helm release is not found
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
`resoure/helm_release`: fix an issue where `postrender.args` is not parsed correctly.
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
change `set.value` && `set_list.value` to optional instead of required
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
```release-note:bug
|
||||||
|
`helm_release`: Fix namespace behaviour for dependency charts in non-default namespaces
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
`helm_release`: Add `set_wo` write-only attribute
|
||||||
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue