Add cloudhost-platform Helm chart and registry ingress manifests.
Deploy backend, frontend, PostgreSQL, and Redis on Kubernetes with optional Ingress/TLS, SQL migration hooks, and public registry exposure at repo.3fase.ir. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -177,6 +177,20 @@ ACTIVE ──(expires)──► SUSPENDED ──(grace)──► PENDING_DELETIO
|
||||
|
||||
---
|
||||
|
||||
## Helm Chart: cloudhost-platform
|
||||
|
||||
Chart at `backend/helm/cloudhost-platform/` deploys the **control plane** (NestJS API, Next.js UI, PostgreSQL, Redis) into a dedicated namespace (default `cloudhost`).
|
||||
|
||||
| Value | Purpose |
|
||||
|-------|---------|
|
||||
| `ingress.enabled` | Create Ingress (default `true`) |
|
||||
| `ingress.tls.enabled` | cert-manager TLS via `clusterIssuer` |
|
||||
| `ingress.frontend.host` / `ingress.api.host` | Public hostnames |
|
||||
| `postgres.password` / `secrets.jwtSecret` | Credentials (auto-generated if empty on first install) |
|
||||
| `migrations.enabled` | Post-install SQL migration Job |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Helm Chart: cloudhost-app
|
||||
|
||||
Single chart at `backend/helm/cloudhost-app/` handles all runtimes:
|
||||
@@ -208,6 +222,7 @@ host/
|
||||
├── backend/
|
||||
│ ├── Dockerfile
|
||||
│ ├── package.json
|
||||
│ ├── helm/cloudhost-platform/ # Helm chart for control plane
|
||||
│ ├── helm/cloudhost-app/ # Helm chart for user apps
|
||||
│ ├── src/
|
||||
│ │ ├── main.ts / app.module.ts
|
||||
|
||||
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
#### Helm Chart (`backend/helm/cloudhost-platform/`)
|
||||
- **Platform chart** — deploy control plane (backend, frontend, PostgreSQL, Redis) on Kubernetes
|
||||
- Ingress with `ingress.enabled` / `ingress.tls.enabled` and cert-manager TLS
|
||||
- Post-install migration Job for SQL files in `migrations/`
|
||||
- `values-production.example.yaml` for production overrides
|
||||
|
||||
#### Helm Chart (`backend/helm/cloudhost-app/`)
|
||||
- **New Helm chart** replacing legacy Handlebars templates for all deployments
|
||||
- `deployment.yaml` — app deployment with `imagePullSecrets`, `imagePullPolicy: Always`, health probes
|
||||
|
||||
@@ -70,7 +70,8 @@ host/
|
||||
│ ├── Dockerfile
|
||||
│ ├── package.json
|
||||
│ ├── helm/
|
||||
│ │ └── cloudhost-app/ # Helm chart (all runtimes)
|
||||
│ │ ├── cloudhost-platform/ # Helm chart (control plane)
|
||||
│ │ └── cloudhost-app/ # Helm chart (user apps)
|
||||
│ │ ├── Chart.yaml
|
||||
│ │ ├── values.yaml
|
||||
│ │ └── templates/ # K8s manifest templates
|
||||
@@ -147,7 +148,29 @@ docker compose up --build
|
||||
|
||||
Backend at port 4000, Frontend at port 3000.
|
||||
|
||||
### 4. Run Locally (development)
|
||||
### 4. Deploy Platform on Kubernetes (Helm)
|
||||
|
||||
Prerequisites: NGINX Ingress, cert-manager (if TLS enabled), StorageClass for PVCs.
|
||||
|
||||
```bash
|
||||
# Build images (set API URL to match ingress.api.host when TLS is on)
|
||||
export REG=your-registry.example.com
|
||||
docker build -t $REG/cloudhost-backend:latest ./backend
|
||||
docker build -t $REG/cloudhost-frontend:latest \
|
||||
--build-arg NEXT_PUBLIC_API_URL=https://api.platform.example.com ./frontend
|
||||
docker push $REG/cloudhost-backend:latest $REG/cloudhost-frontend:latest
|
||||
|
||||
# Install (copy and edit values-production.example.yaml first)
|
||||
helm upgrade --install cloudhost ./backend/helm/cloudhost-platform \
|
||||
-n cloudhost --create-namespace \
|
||||
-f backend/helm/cloudhost-platform/values-production.example.yaml
|
||||
```
|
||||
|
||||
Key values: `ingress.enabled`, `ingress.tls.enabled`, `ingress.frontend.host`, `ingress.api.host`, `postgres.password`, `secrets.jwtSecret`.
|
||||
|
||||
See chart defaults in `backend/helm/cloudhost-platform/values.yaml` and post-install notes via `helm get notes cloudhost -n cloudhost`.
|
||||
|
||||
### 5. Run Locally (development)
|
||||
|
||||
```bash
|
||||
# Terminal 1 — Backend
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Patterns to ignore when packaging chart
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.bak
|
||||
values-production.yaml
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: cloudhost-platform
|
||||
description: CloudHost control plane — backend, frontend, PostgreSQL, Redis
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "1.0.0"
|
||||
@@ -0,0 +1,37 @@
|
||||
# cloudhost-platform
|
||||
|
||||
Helm chart for the CloudHost control plane: backend API, frontend UI, PostgreSQL, and Redis.
|
||||
|
||||
## Quick install
|
||||
|
||||
```bash
|
||||
helm upgrade --install cloudhost . \
|
||||
-n cloudhost --create-namespace \
|
||||
-f values-production.example.yaml
|
||||
```
|
||||
|
||||
## Ingress / TLS
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `ingress.enabled` | Create Ingress resources |
|
||||
| `ingress.tls.enabled` | Enable cert-manager TLS |
|
||||
| `ingress.tls.clusterIssuer` | ClusterIssuer name (e.g. `letsencrypt-prod`) |
|
||||
| `ingress.frontend.host` | UI hostname |
|
||||
| `ingress.api.host` | API hostname |
|
||||
|
||||
Requires NGINX Ingress Controller and cert-manager when TLS is enabled.
|
||||
|
||||
## Frontend image
|
||||
|
||||
Build with the public API URL baked in:
|
||||
|
||||
```bash
|
||||
docker build -t $REG/cloudhost-frontend:tag \
|
||||
--build-arg NEXT_PUBLIC_API_URL=https://api.platform.example.com \
|
||||
../../frontend
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
SQL files in `migrations/` run via a post-install/post-upgrade Job when `migrations.enabled` is true. They assume incremental schema changes on top of an existing database; for a completely empty database you may need to bootstrap schema first (e.g. one-time `NODE_ENV=development` or manual setup).
|
||||
@@ -0,0 +1,40 @@
|
||||
-- Temporary external access grants (Redis, RabbitMQ, database)
|
||||
CREATE TYPE service_access_target AS ENUM (
|
||||
'database',
|
||||
'redis',
|
||||
'rabbitmq_amqp',
|
||||
'rabbitmq_management'
|
||||
);
|
||||
|
||||
CREATE TYPE service_access_grant_status AS ENUM (
|
||||
'active',
|
||||
'expired',
|
||||
'revoked'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS service_access_grants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"applicationId" UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||
"userId" UUID NOT NULL,
|
||||
"clusterId" UUID NOT NULL,
|
||||
namespace VARCHAR(255) NOT NULL,
|
||||
target service_access_target NOT NULL,
|
||||
"nodePort" INTEGER NOT NULL,
|
||||
"targetPort" INTEGER NOT NULL,
|
||||
host VARCHAR(255) NOT NULL,
|
||||
"k8sServiceName" VARCHAR(255) NOT NULL,
|
||||
status service_access_grant_status NOT NULL DEFAULT 'active',
|
||||
"expiresAt" TIMESTAMPTZ NOT NULL,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_service_access_grants_app_target_status
|
||||
ON service_access_grants ("applicationId", target, status);
|
||||
|
||||
INSERT INTO platform_settings (id, key, value, description, "createdAt", "updatedAt")
|
||||
SELECT gen_random_uuid(), 'access_max_duration_minutes', '240',
|
||||
'Maximum duration (minutes) for temporary external service access',
|
||||
NOW(), NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM platform_settings WHERE key = 'access_max_duration_minutes'
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Add DOCKED lifecycle status and dock metadata (run if TypeORM sync is disabled)
|
||||
ALTER TYPE applications_lifecyclestatus_enum ADD VALUE IF NOT EXISTS 'docked';
|
||||
|
||||
ALTER TABLE applications
|
||||
ADD COLUMN IF NOT EXISTS "dockedAt" TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS "dockSnapshotId" VARCHAR;
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE IF NOT EXISTS resource_credits (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"userId" UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
"sourceAppName" VARCHAR,
|
||||
runtime VARCHAR NOT NULL,
|
||||
"databaseType" VARCHAR NOT NULL,
|
||||
"cpuLimit" VARCHAR NOT NULL,
|
||||
"memoryLimit" VARCHAR NOT NULL,
|
||||
replicas INT NOT NULL DEFAULT 1,
|
||||
"dbStorageSize" VARCHAR,
|
||||
"appStorageSize" VARCHAR,
|
||||
"enableRedis" BOOLEAN NOT NULL DEFAULT false,
|
||||
"enableRabbitmq" BOOLEAN NOT NULL DEFAULT false,
|
||||
"enableElasticsearch" BOOLEAN NOT NULL DEFAULT false,
|
||||
"billingCycle" VARCHAR,
|
||||
"expiresAt" TIMESTAMPTZ NOT NULL,
|
||||
"consumedAt" TIMESTAMPTZ,
|
||||
"appliedApplicationId" VARCHAR,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_resource_credits_user_active
|
||||
ON resource_credits ("userId", "expiresAt")
|
||||
WHERE "consumedAt" IS NULL;
|
||||
@@ -0,0 +1,117 @@
|
||||
-- Usage-based pricing catalog (replaces per-cycle service_plans + scattered platform_settings)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pricing_rates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
runtime VARCHAR NOT NULL,
|
||||
resource_type VARCHAR NOT NULL,
|
||||
hourly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
monthly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
yearly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (runtime, resource_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS addon_rates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
resource_type VARCHAR NOT NULL UNIQUE,
|
||||
hourly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
monthly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
yearly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Seed addon rows
|
||||
INSERT INTO addon_rates (resource_type, hourly_price, monthly_price, yearly_price)
|
||||
VALUES
|
||||
('redis_addon', 0, 0, 0),
|
||||
('rabbitmq_addon', 0, 0, 0),
|
||||
('elasticsearch_addon', 0, 0, 0),
|
||||
('custom_domain_addon', 0, 0, 0)
|
||||
ON CONFLICT (resource_type) DO NOTHING;
|
||||
|
||||
-- Migrate runtime rates from service_plans + pricing_rules
|
||||
INSERT INTO pricing_rates (runtime, resource_type, hourly_price, monthly_price, yearly_price, is_active)
|
||||
SELECT
|
||||
sp.runtime,
|
||||
pr.resource_type,
|
||||
COALESCE(MAX(CASE WHEN sp."billingCycle" = 'hourly' THEN pr.unit_price END), 0),
|
||||
COALESCE(MAX(CASE WHEN sp."billingCycle" = 'monthly' THEN pr.unit_price END), 0),
|
||||
COALESCE(MAX(CASE WHEN sp."billingCycle" = 'yearly' THEN pr.unit_price END), 0),
|
||||
BOOL_OR(sp."isActive")
|
||||
FROM pricing_rules pr
|
||||
JOIN service_plans sp ON sp.id = pr."planId"
|
||||
WHERE pr.resource_type IN (
|
||||
'base_fee', 'cpu_per_core', 'memory_per_gb', 'storage_per_gb', 'database_addon'
|
||||
)
|
||||
GROUP BY sp.runtime, pr.resource_type
|
||||
ON CONFLICT (runtime, resource_type) DO UPDATE SET
|
||||
hourly_price = EXCLUDED.hourly_price,
|
||||
monthly_price = EXCLUDED.monthly_price,
|
||||
yearly_price = EXCLUDED.yearly_price,
|
||||
is_active = EXCLUDED.is_active,
|
||||
"updatedAt" = NOW();
|
||||
|
||||
-- Optional services from platform_settings JSON
|
||||
UPDATE addon_rates ar SET
|
||||
hourly_price = COALESCE((s.parsed->'redis'->>'hourly')::decimal, 0),
|
||||
monthly_price = COALESCE((s.parsed->'redis'->>'monthly')::decimal, 0),
|
||||
yearly_price = COALESCE((s.parsed->'redis'->>'yearly')::decimal, 0)
|
||||
FROM (
|
||||
SELECT value::jsonb AS parsed
|
||||
FROM platform_settings
|
||||
WHERE key = 'optional_services_pricing_toman'
|
||||
LIMIT 1
|
||||
) s
|
||||
WHERE ar.resource_type = 'redis_addon' AND s.parsed IS NOT NULL;
|
||||
|
||||
UPDATE addon_rates ar SET
|
||||
hourly_price = COALESCE((s.parsed->'rabbitmq'->>'hourly')::decimal, 0),
|
||||
monthly_price = COALESCE((s.parsed->'rabbitmq'->>'monthly')::decimal, 0),
|
||||
yearly_price = COALESCE((s.parsed->'rabbitmq'->>'yearly')::decimal, 0)
|
||||
FROM (
|
||||
SELECT value::jsonb AS parsed
|
||||
FROM platform_settings
|
||||
WHERE key = 'optional_services_pricing_toman'
|
||||
LIMIT 1
|
||||
) s
|
||||
WHERE ar.resource_type = 'rabbitmq_addon' AND s.parsed IS NOT NULL;
|
||||
|
||||
UPDATE addon_rates ar SET
|
||||
hourly_price = COALESCE((s.parsed->'elasticsearch'->>'hourly')::decimal, 0),
|
||||
monthly_price = COALESCE((s.parsed->'elasticsearch'->>'monthly')::decimal, 0),
|
||||
yearly_price = COALESCE((s.parsed->'elasticsearch'->>'yearly')::decimal, 0)
|
||||
FROM (
|
||||
SELECT value::jsonb AS parsed
|
||||
FROM platform_settings
|
||||
WHERE key = 'optional_services_pricing_toman'
|
||||
LIMIT 1
|
||||
) s
|
||||
WHERE ar.resource_type = 'elasticsearch_addon' AND s.parsed IS NOT NULL;
|
||||
|
||||
UPDATE addon_rates ar SET
|
||||
monthly_price = COALESCE(s.price, 0)
|
||||
FROM (
|
||||
SELECT value::decimal AS price
|
||||
FROM platform_settings
|
||||
WHERE key = 'custom_domain_monthly_price_toman'
|
||||
LIMIT 1
|
||||
) s
|
||||
WHERE ar.resource_type = 'custom_domain_addon' AND s.price IS NOT NULL;
|
||||
|
||||
-- Default runtime rows for nodejs, laravel, wordpress
|
||||
INSERT INTO pricing_rates (runtime, resource_type, hourly_price, monthly_price, yearly_price)
|
||||
SELECT r.runtime, t.resource_type, 0, 0, 0
|
||||
FROM (VALUES ('nodejs'), ('laravel'), ('wordpress')) AS r(runtime)
|
||||
CROSS JOIN (
|
||||
VALUES
|
||||
('base_fee'),
|
||||
('cpu_per_core'),
|
||||
('memory_per_gb'),
|
||||
('storage_per_gb'),
|
||||
('database_addon')
|
||||
) AS t(resource_type)
|
||||
ON CONFLICT (runtime, resource_type) DO NOTHING;
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Ensure pricing_rates rows exist for every AppRuntime enum value
|
||||
|
||||
INSERT INTO pricing_rates (runtime, resource_type, hourly_price, monthly_price, yearly_price)
|
||||
SELECT r.runtime, t.resource_type, 0, 0, 0
|
||||
FROM (
|
||||
VALUES
|
||||
('nodejs'),
|
||||
('laravel'),
|
||||
('wordpress'),
|
||||
('go'),
|
||||
('php'),
|
||||
('python'),
|
||||
('django'),
|
||||
('dotnet')
|
||||
) AS r(runtime)
|
||||
CROSS JOIN (
|
||||
VALUES
|
||||
('base_fee'),
|
||||
('cpu_per_core'),
|
||||
('memory_per_gb'),
|
||||
('storage_per_gb'),
|
||||
('database_addon')
|
||||
) AS t(resource_type)
|
||||
ON CONFLICT (runtime, resource_type) DO NOTHING;
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Per-addon consumption footprint and optional resource unit prices (0 = use app runtime rates)
|
||||
|
||||
ALTER TABLE addon_rates
|
||||
ADD COLUMN IF NOT EXISTS cpu_limit VARCHAR,
|
||||
ADD COLUMN IF NOT EXISTS memory_limit VARCHAR,
|
||||
ADD COLUMN IF NOT EXISTS storage_gi DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS log_shipper_cpu_limit VARCHAR,
|
||||
ADD COLUMN IF NOT EXISTS log_shipper_memory_limit VARCHAR,
|
||||
ADD COLUMN IF NOT EXISTS cpu_hourly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS cpu_monthly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS cpu_yearly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS memory_hourly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS memory_monthly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS memory_yearly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS storage_hourly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS storage_monthly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS storage_yearly_price DECIMAL(12, 2) NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE addon_rates SET cpu_limit = '200m', memory_limit = '256Mi', storage_gi = 1
|
||||
WHERE resource_type = 'redis_addon' AND cpu_limit IS NULL;
|
||||
|
||||
UPDATE addon_rates SET cpu_limit = '500m', memory_limit = '512Mi', storage_gi = 2
|
||||
WHERE resource_type = 'rabbitmq_addon' AND cpu_limit IS NULL;
|
||||
|
||||
UPDATE addon_rates SET cpu_limit = '50m', memory_limit = '64Mi', storage_gi = 0,
|
||||
log_shipper_cpu_limit = '50m', log_shipper_memory_limit = '64Mi'
|
||||
WHERE resource_type = 'elasticsearch_addon' AND cpu_limit IS NULL;
|
||||
@@ -0,0 +1,98 @@
|
||||
-- Optional services: resource profile + pricing matrix (same model as application runtimes)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS optional_service_profiles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
service VARCHAR NOT NULL UNIQUE,
|
||||
cpu_limit VARCHAR NOT NULL DEFAULT '200m',
|
||||
memory_limit VARCHAR NOT NULL DEFAULT '256Mi',
|
||||
storage_gi DECIMAL(10, 2) NOT NULL DEFAULT 0,
|
||||
log_shipper_cpu_limit VARCHAR,
|
||||
log_shipper_memory_limit VARCHAR,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS optional_service_rates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
service VARCHAR NOT NULL,
|
||||
resource_type VARCHAR NOT NULL,
|
||||
hourly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
monthly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
yearly_price DECIMAL(12, 2) NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (service, resource_type)
|
||||
);
|
||||
|
||||
INSERT INTO optional_service_profiles (service, cpu_limit, memory_limit, storage_gi, log_shipper_cpu_limit, log_shipper_memory_limit)
|
||||
VALUES
|
||||
('redis', '200m', '256Mi', 1, NULL, NULL),
|
||||
('rabbitmq', '500m', '512Mi', 2, NULL, NULL),
|
||||
('elasticsearch', '50m', '64Mi', 0, '50m', '64Mi')
|
||||
ON CONFLICT (service) DO NOTHING;
|
||||
|
||||
UPDATE optional_service_profiles SET
|
||||
log_shipper_cpu_limit = '50m',
|
||||
log_shipper_memory_limit = '64Mi'
|
||||
WHERE service = 'elasticsearch' AND log_shipper_cpu_limit IS NULL;
|
||||
|
||||
INSERT INTO optional_service_rates (service, resource_type, hourly_price, monthly_price, yearly_price)
|
||||
SELECT s.service, t.resource_type, 0, 0, 0
|
||||
FROM (VALUES ('redis'), ('rabbitmq'), ('elasticsearch')) AS s(service)
|
||||
CROSS JOIN (
|
||||
VALUES ('base_fee'), ('cpu_per_core'), ('memory_per_gb'), ('storage_per_gb')
|
||||
) AS t(resource_type)
|
||||
ON CONFLICT (service, resource_type) DO NOTHING;
|
||||
|
||||
-- Migrate legacy per-resource addon prices into optional_service_rates (if columns exist)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'addon_rates' AND column_name = 'cpu_hourly_price'
|
||||
) THEN
|
||||
UPDATE optional_service_rates osr SET
|
||||
hourly_price = COALESCE(ar.cpu_hourly_price, 0),
|
||||
monthly_price = COALESCE(ar.cpu_monthly_price, 0),
|
||||
yearly_price = COALESCE(ar.cpu_yearly_price, 0)
|
||||
FROM addon_rates ar
|
||||
WHERE ar.resource_type = 'redis_addon' AND osr.service = 'redis' AND osr.resource_type = 'cpu_per_core'
|
||||
AND (ar.cpu_hourly_price > 0 OR ar.cpu_monthly_price > 0);
|
||||
|
||||
UPDATE optional_service_rates osr SET
|
||||
hourly_price = COALESCE(ar.memory_hourly_price, 0),
|
||||
monthly_price = COALESCE(ar.memory_monthly_price, 0),
|
||||
yearly_price = COALESCE(ar.memory_yearly_price, 0)
|
||||
FROM addon_rates ar
|
||||
WHERE ar.resource_type = 'redis_addon' AND osr.service = 'redis' AND osr.resource_type = 'memory_per_gb'
|
||||
AND (ar.memory_hourly_price > 0 OR ar.memory_monthly_price > 0);
|
||||
|
||||
UPDATE optional_service_rates osr SET
|
||||
hourly_price = COALESCE(ar.storage_hourly_price, 0),
|
||||
monthly_price = COALESCE(ar.storage_monthly_price, 0),
|
||||
yearly_price = COALESCE(ar.storage_yearly_price, 0)
|
||||
FROM addon_rates ar
|
||||
WHERE ar.resource_type = 'redis_addon' AND osr.service = 'redis' AND osr.resource_type = 'storage_per_gb'
|
||||
AND (ar.storage_hourly_price > 0 OR ar.storage_monthly_price > 0);
|
||||
|
||||
UPDATE optional_service_profiles osp SET
|
||||
cpu_limit = COALESCE(ar.cpu_limit, osp.cpu_limit),
|
||||
memory_limit = COALESCE(ar.memory_limit, osp.memory_limit),
|
||||
storage_gi = COALESCE(ar.storage_gi, osp.storage_gi)
|
||||
FROM addon_rates ar WHERE ar.resource_type = 'redis_addon' AND osp.service = 'redis';
|
||||
|
||||
UPDATE optional_service_profiles osp SET
|
||||
cpu_limit = COALESCE(ar.cpu_limit, osp.cpu_limit),
|
||||
memory_limit = COALESCE(ar.memory_limit, osp.memory_limit),
|
||||
storage_gi = COALESCE(ar.storage_gi, osp.storage_gi)
|
||||
FROM addon_rates ar WHERE ar.resource_type = 'rabbitmq_addon' AND osp.service = 'rabbitmq';
|
||||
|
||||
UPDATE optional_service_profiles osp SET
|
||||
cpu_limit = COALESCE(ar.cpu_limit, osp.cpu_limit),
|
||||
memory_limit = COALESCE(ar.memory_limit, osp.memory_limit),
|
||||
log_shipper_cpu_limit = COALESCE(ar.log_shipper_cpu_limit, osp.log_shipper_cpu_limit),
|
||||
log_shipper_memory_limit = COALESCE(ar.log_shipper_memory_limit, osp.log_shipper_memory_limit)
|
||||
FROM addon_rates ar WHERE ar.resource_type = 'elasticsearch_addon' AND osp.service = 'elasticsearch';
|
||||
END IF;
|
||||
END $$;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
-- Per-deploy optional service resource limits (user-chosen, like application CPU/RAM/storage)
|
||||
|
||||
ALTER TABLE applications
|
||||
ADD COLUMN IF NOT EXISTS optional_service_resources JSONB;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Deploy-wizard default requests (TypeORM camelCase column names)
|
||||
|
||||
ALTER TABLE optional_service_profiles
|
||||
ADD COLUMN IF NOT EXISTS "cpuRequest" VARCHAR,
|
||||
ADD COLUMN IF NOT EXISTS "memoryRequest" VARCHAR;
|
||||
|
||||
UPDATE optional_service_profiles SET "cpuRequest" = '50m', "memoryRequest" = '64Mi'
|
||||
WHERE service = 'redis' AND "cpuRequest" IS NULL;
|
||||
|
||||
UPDATE optional_service_profiles SET "cpuRequest" = '100m', "memoryRequest" = '256Mi'
|
||||
WHERE service = 'rabbitmq' AND "cpuRequest" IS NULL;
|
||||
|
||||
UPDATE optional_service_profiles SET "cpuRequest" = '50m', "memoryRequest" = '64Mi'
|
||||
WHERE service = 'elasticsearch' AND "cpuRequest" IS NULL;
|
||||
@@ -0,0 +1,52 @@
|
||||
CREATE TABLE IF NOT EXISTS invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"invoiceNumber" VARCHAR NOT NULL UNIQUE,
|
||||
"userId" UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
"applicationId" UUID REFERENCES applications(id) ON DELETE SET NULL,
|
||||
reason VARCHAR NOT NULL DEFAULT 'manual',
|
||||
status VARCHAR NOT NULL DEFAULT 'issued',
|
||||
"paymentMethod" VARCHAR,
|
||||
subtotal DECIMAL(14, 2) NOT NULL DEFAULT 0,
|
||||
total DECIMAL(14, 2) NOT NULL DEFAULT 0,
|
||||
"paidAmount" DECIMAL(14, 2) NOT NULL DEFAULT 0,
|
||||
"dueAmount" DECIMAL(14, 2) NOT NULL DEFAULT 0,
|
||||
"dueDate" TIMESTAMPTZ,
|
||||
"paidAt" TIMESTAMPTZ,
|
||||
"gatewayTrackingCode" VARCHAR,
|
||||
"gatewayReference" VARCHAR,
|
||||
"adminNote" VARCHAR,
|
||||
"statusReason" VARCHAR,
|
||||
metadata JSONB,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invoice_lines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"invoiceId" UUID NOT NULL REFERENCES invoices(id) ON DELETE CASCADE,
|
||||
label VARCHAR NOT NULL,
|
||||
description VARCHAR,
|
||||
quantity INT NOT NULL DEFAULT 1,
|
||||
"unitAmount" DECIMAL(14, 2) NOT NULL DEFAULT 0,
|
||||
amount DECIMAL(14, 2) NOT NULL DEFAULT 0,
|
||||
metadata JSONB,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE wallet_transactions
|
||||
ADD COLUMN IF NOT EXISTS "invoiceId" UUID REFERENCES invoices(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE wallet_transactions
|
||||
ADD COLUMN IF NOT EXISTS "gatewayTrackingCode" VARCHAR;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_user_status_created
|
||||
ON invoices ("userId", status, "createdAt" DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_invoices_application
|
||||
ON invoices ("applicationId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_invoice_lines_invoice
|
||||
ON invoice_lines ("invoiceId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_wallet_transactions_invoice
|
||||
ON wallet_transactions ("invoiceId");
|
||||
@@ -0,0 +1,10 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_type
|
||||
WHERE typname = 'deployments_status_enum'
|
||||
) THEN
|
||||
ALTER TYPE deployments_status_enum ADD VALUE IF NOT EXISTS 'cancelled';
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,113 @@
|
||||
-- Cluster pool allocation, health snapshots, and audit logs.
|
||||
|
||||
ALTER TABLE clusters
|
||||
ADD COLUMN IF NOT EXISTS weight INTEGER NOT NULL DEFAULT 1,
|
||||
ADD COLUMN IF NOT EXISTS tags JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
ADD COLUMN IF NOT EXISTS "healthStatus" VARCHAR NOT NULL DEFAULT 'unknown',
|
||||
ADD COLUMN IF NOT EXISTS "lastHealthCheckedAt" TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS "healthMessage" VARCHAR,
|
||||
ADD COLUMN IF NOT EXISTS "availableResources" JSONB;
|
||||
|
||||
ALTER TABLE cluster_pools
|
||||
ADD COLUMN IF NOT EXISTS "isDefault" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS priority INTEGER NOT NULL DEFAULT 100;
|
||||
|
||||
ALTER TABLE cluster_pools
|
||||
ALTER COLUMN strategy SET DEFAULT 'weighted-resource';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_clusters_status_health
|
||||
ON clusters(status, "healthStatus");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_clusters_tags
|
||||
ON clusters USING GIN(tags);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cluster_pools_default_priority
|
||||
ON cluster_pools("isDefault", priority)
|
||||
WHERE "isActive" = TRUE;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cluster_health (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"clusterId" UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE,
|
||||
status VARCHAR NOT NULL DEFAULT 'unknown',
|
||||
"readyNodes" INTEGER NOT NULL DEFAULT 0,
|
||||
"nodeCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"cpuAllocatable" VARCHAR,
|
||||
"memoryAllocatable" VARCHAR,
|
||||
"podCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"appCount" INTEGER NOT NULL DEFAULT 0,
|
||||
message VARCHAR,
|
||||
resources JSONB,
|
||||
"checkedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cluster_health_cluster_checked
|
||||
ON cluster_health("clusterId", "checkedAt" DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cluster_allocation_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"applicationId" UUID REFERENCES applications(id) ON DELETE SET NULL,
|
||||
"userId" UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
"poolId" UUID REFERENCES cluster_pools(id) ON DELETE SET NULL,
|
||||
"selectedClusterId" UUID REFERENCES clusters(id) ON DELETE SET NULL,
|
||||
strategy VARCHAR NOT NULL DEFAULT 'weighted-resource',
|
||||
"estimatedRequest" JSONB,
|
||||
"candidateScores" JSONB,
|
||||
"rejectionReasons" JSONB,
|
||||
status VARCHAR NOT NULL DEFAULT 'success',
|
||||
message VARCHAR,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cluster_allocation_logs_user_created
|
||||
ON cluster_allocation_logs("userId", "createdAt" DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cluster_allocation_logs_app
|
||||
ON cluster_allocation_logs("applicationId");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_cluster_allocation_logs_cluster_created
|
||||
ON cluster_allocation_logs("selectedClusterId", "createdAt" DESC);
|
||||
|
||||
ALTER TABLE applications
|
||||
ADD COLUMN IF NOT EXISTS "clusterId" UUID,
|
||||
ADD COLUMN IF NOT EXISTS "poolId" UUID;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'applications' AND column_name = 'clusterId' AND data_type <> 'uuid'
|
||||
) THEN
|
||||
ALTER TABLE applications
|
||||
ALTER COLUMN "clusterId" TYPE UUID USING NULLIF("clusterId", '')::uuid;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'applications' AND column_name = 'poolId' AND data_type <> 'uuid'
|
||||
) THEN
|
||||
ALTER TABLE applications
|
||||
ALTER COLUMN "poolId" TYPE UUID USING NULLIF("poolId", '')::uuid;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_applications_cluster'
|
||||
) THEN
|
||||
ALTER TABLE applications
|
||||
ADD CONSTRAINT fk_applications_cluster FOREIGN KEY ("clusterId")
|
||||
REFERENCES clusters(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_applications_pool'
|
||||
) THEN
|
||||
ALTER TABLE applications
|
||||
ADD CONSTRAINT fk_applications_pool FOREIGN KEY ("poolId")
|
||||
REFERENCES cluster_pools(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_applications_cluster_id ON applications("clusterId");
|
||||
CREATE INDEX IF NOT EXISTS idx_applications_pool_id ON applications("poolId");
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE IF NOT EXISTS application_migration_jobs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"applicationId" UUID NOT NULL REFERENCES applications(id) ON DELETE CASCADE,
|
||||
"requestedBy" UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
"sourceClusterId" UUID NOT NULL REFERENCES clusters(id) ON DELETE RESTRICT,
|
||||
"targetClusterId" UUID NOT NULL REFERENCES clusters(id) ON DELETE RESTRICT,
|
||||
status VARCHAR NOT NULL DEFAULT 'queued',
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
"maxAttempts" INTEGER NOT NULL DEFAULT 3,
|
||||
"currentStep" VARCHAR,
|
||||
"errorMessage" VARCHAR,
|
||||
metadata JSONB,
|
||||
"startedAt" TIMESTAMPTZ,
|
||||
"completedAt" TIMESTAMPTZ,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_application_migration_jobs_app_created
|
||||
ON application_migration_jobs("applicationId", "createdAt" DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_application_migration_jobs_status
|
||||
ON application_migration_jobs(status);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS application_migration_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
"migrationId" UUID NOT NULL REFERENCES application_migration_jobs(id) ON DELETE CASCADE,
|
||||
step VARCHAR NOT NULL,
|
||||
level VARCHAR NOT NULL DEFAULT 'info',
|
||||
message VARCHAR NOT NULL,
|
||||
metadata JSONB,
|
||||
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_application_migration_events_migration_created
|
||||
ON application_migration_events("migrationId", "createdAt" ASC);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Standalone managed services (database / redis / rabbitmq) vs full applications
|
||||
ALTER TABLE applications
|
||||
ADD COLUMN IF NOT EXISTS product_type VARCHAR(32) NOT NULL DEFAULT 'application';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_applications_user_product_type
|
||||
ON applications (user_id, product_type);
|
||||
|
||||
ALTER TABLE resource_credits
|
||||
ADD COLUMN IF NOT EXISTS product_type VARCHAR(32) NOT NULL DEFAULT 'application';
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE service_access_grants
|
||||
ADD COLUMN IF NOT EXISTS persistent BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Snapshot creation progress (0–100) for UI feedback during DB dumps
|
||||
ALTER TABLE snapshots ADD COLUMN IF NOT EXISTS progress INT NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,37 @@
|
||||
CloudHost platform has been deployed.
|
||||
|
||||
Release: {{ .Release.Name }}
|
||||
Namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
|
||||
{{- if .Values.ingress.enabled }}
|
||||
|
||||
Public URLs (configure DNS to point at your Ingress controller):
|
||||
Frontend: {{ include "cloudhost-platform.frontendPublicUrl" . }}
|
||||
API: {{ include "cloudhost-platform.apiPublicUrl" . }}
|
||||
|
||||
Build the frontend image with:
|
||||
docker build -t <registry>/cloudhost-frontend:<tag> \
|
||||
--build-arg NEXT_PUBLIC_API_URL={{ include "cloudhost-platform.apiPublicUrl" . }} \
|
||||
./frontend
|
||||
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
TLS: enabled (cert-manager issuer: {{ .Values.ingress.tls.clusterIssuer }})
|
||||
Ensure cert-manager and ClusterIssuer "{{ .Values.ingress.tls.clusterIssuer }}" exist.
|
||||
{{- else }}
|
||||
TLS: disabled (HTTP only)
|
||||
{{- end }}
|
||||
|
||||
{{- else }}
|
||||
|
||||
Ingress is disabled. Port-forward to access services:
|
||||
kubectl port-forward -n {{ include "cloudhost-platform.namespace" . }} svc/{{ include "cloudhost-platform.frontend.fullname" . }} 3000:3000
|
||||
kubectl port-forward -n {{ include "cloudhost-platform.namespace" . }} svc/{{ include "cloudhost-platform.backend.fullname" . }} 4000:4000
|
||||
|
||||
{{- end }}
|
||||
|
||||
Backend CORS (FRONTEND_URL): {{ include "cloudhost-platform.frontendPublicUrl" . }}
|
||||
|
||||
After install, register your Kubernetes cluster in the admin panel (Clusters) so deployments can run.
|
||||
|
||||
To retrieve auto-generated secrets:
|
||||
kubectl get secret -n {{ include "cloudhost-platform.namespace" . }} {{ include "cloudhost-platform.secretName" . }} -o jsonpath='{.data.postgres-password}' | base64 -d; echo
|
||||
@@ -0,0 +1,96 @@
|
||||
{{- define "cloudhost-platform.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.namespace" -}}
|
||||
{{- .Values.namespace | default "cloudhost" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.labels" -}}
|
||||
helm.sh/chart: {{ include "cloudhost-platform.chart" . }}
|
||||
{{ include "cloudhost-platform.selectorLabels" . }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "cloudhost-platform.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.postgres.fullname" -}}
|
||||
{{- printf "%s-postgres" (include "cloudhost-platform.fullname" .) }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.redis.fullname" -}}
|
||||
{{- printf "%s-redis" (include "cloudhost-platform.fullname" .) }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.backend.fullname" -}}
|
||||
{{- printf "%s-backend" (include "cloudhost-platform.fullname" .) }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.frontend.fullname" -}}
|
||||
{{- printf "%s-frontend" (include "cloudhost-platform.fullname" .) }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.secretName" -}}
|
||||
{{- printf "%s-secrets" (include "cloudhost-platform.fullname" .) }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.tlsSecretName" -}}
|
||||
{{- if .Values.ingress.tls.secretName }}
|
||||
{{- .Values.ingress.tls.secretName }}
|
||||
{{- else }}
|
||||
{{- printf "%s-platform-tls" (include "cloudhost-platform.fullname" .) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.urlScheme" -}}
|
||||
{{- if and .Values.ingress.enabled .Values.ingress.tls.enabled }}https{{- else }}http{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.frontendPublicUrl" -}}
|
||||
{{- $scheme := include "cloudhost-platform.urlScheme" . -}}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- if .Values.ingress.singleHost.enabled }}
|
||||
{{- printf "%s://%s" $scheme .Values.ingress.singleHost.host }}
|
||||
{{- else }}
|
||||
{{- printf "%s://%s" $scheme .Values.ingress.frontend.host }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- printf "http://%s-frontend.%s.svc.cluster.local:3000" (include "cloudhost-platform.fullname" .) (include "cloudhost-platform.namespace" .) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.apiPublicUrl" -}}
|
||||
{{- $scheme := include "cloudhost-platform.urlScheme" . -}}
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- if .Values.ingress.singleHost.enabled }}
|
||||
{{- printf "%s://%s%s" $scheme .Values.ingress.singleHost.host .Values.ingress.singleHost.apiPath }}
|
||||
{{- else }}
|
||||
{{- printf "%s://%s" $scheme .Values.ingress.api.host }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
{{- printf "http://%s-backend.%s.svc.cluster.local:4000" (include "cloudhost-platform.fullname" .) (include "cloudhost-platform.namespace" .) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.backendImage" -}}
|
||||
{{- printf "%s:%s" .Values.images.backend.repository .Values.images.backend.tag }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "cloudhost-platform.frontendImage" -}}
|
||||
{{- printf "%s:%s" .Values.images.frontend.repository .Values.images.frontend.tag }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,116 @@
|
||||
{{- if .Values.backend.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.backend.replicas }}
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
spec:
|
||||
initContainers:
|
||||
{{- if .Values.postgres.enabled }}
|
||||
- name: wait-postgres
|
||||
image: {{ .Values.images.busybox | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
until nc -z {{ include "cloudhost-platform.postgres.fullname" . }} 5432; do
|
||||
echo "waiting for postgres..."
|
||||
sleep 2
|
||||
done
|
||||
{{- end }}
|
||||
{{- if .Values.redis.enabled }}
|
||||
- name: wait-redis
|
||||
image: {{ .Values.images.busybox | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
until nc -z {{ include "cloudhost-platform.redis.fullname" . }} 6379; do
|
||||
echo "waiting for redis..."
|
||||
sleep 2
|
||||
done
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: backend
|
||||
image: {{ include "cloudhost-platform.backendImage" . | quote }}
|
||||
imagePullPolicy: {{ .Values.images.backend.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 4000
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_USERNAME
|
||||
value: {{ .Values.postgres.username | quote }}
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "cloudhost-platform.secretName" . }}
|
||||
key: postgres-password
|
||||
- name: DB_DATABASE
|
||||
value: {{ .Values.postgres.database | quote }}
|
||||
- name: REDIS_HOST
|
||||
value: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
- name: REDIS_PORT
|
||||
value: "6379"
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "cloudhost-platform.secretName" . }}
|
||||
key: jwt-secret
|
||||
- name: JWT_REFRESH_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "cloudhost-platform.secretName" . }}
|
||||
key: jwt-refresh-secret
|
||||
- name: FRONTEND_URL
|
||||
value: {{ include "cloudhost-platform.frontendPublicUrl" . | quote }}
|
||||
{{- range $key, $val := .Values.backend.env }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
{{- range $key, $val := .Values.backend.extraEnv }}
|
||||
- name: {{ $key }}
|
||||
value: {{ $val | quote }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: uploads
|
||||
mountPath: /app/uploads
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/docs
|
||||
port: 4000
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/docs
|
||||
port: 4000
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
{{- with .Values.backend.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: uploads
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "cloudhost-platform.backend.fullname" . }}-uploads
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.backend.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.backend.fullname" . }}-uploads
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{- if .Values.global.storageClass }}
|
||||
storageClassName: {{ .Values.global.storageClass | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.backend.uploads.size | quote }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- if .Values.backend.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 4000
|
||||
targetPort: 4000
|
||||
{{- end }}
|
||||
@@ -0,0 +1,47 @@
|
||||
{{- if .Values.frontend.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.frontend.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
spec:
|
||||
containers:
|
||||
- name: frontend
|
||||
image: {{ include "cloudhost-platform.frontendImage" . | quote }}
|
||||
imagePullPolicy: {{ .Values.images.frontend.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: HOSTNAME
|
||||
value: "0.0.0.0"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
{{- with .Values.frontend.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- if .Values.frontend.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
{{- end }}
|
||||
@@ -0,0 +1,73 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
cert-manager.io/cluster-issuer: {{ .Values.ingress.tls.clusterIssuer | quote }}
|
||||
{{- end }}
|
||||
{{- if and .Values.ingress.singleHost.enabled .Values.ingress.singleHost.apiPath }}
|
||||
nginx.ingress.kubernetes.io/use-regex: "true"
|
||||
{{- end }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- if .Values.ingress.tls.enabled }}
|
||||
tls:
|
||||
- hosts:
|
||||
{{- if .Values.ingress.singleHost.enabled }}
|
||||
- {{ .Values.ingress.singleHost.host | quote }}
|
||||
{{- else }}
|
||||
- {{ .Values.ingress.frontend.host | quote }}
|
||||
- {{ .Values.ingress.api.host | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ include "cloudhost-platform.tlsSecretName" . }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- if .Values.ingress.singleHost.enabled }}
|
||||
- host: {{ .Values.ingress.singleHost.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .Values.ingress.singleHost.apiPath }}
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
port:
|
||||
number: 4000
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
port:
|
||||
number: 3000
|
||||
{{- else }}
|
||||
- host: {{ .Values.ingress.frontend.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "cloudhost-platform.frontend.fullname" . }}
|
||||
port:
|
||||
number: 3000
|
||||
- host: {{ .Values.ingress.api.host | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "cloudhost-platform.backend.fullname" . }}
|
||||
port:
|
||||
number: 4000
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,11 @@
|
||||
{{- if and .Values.migrations.enabled .Values.postgres.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.fullname" . }}-migrations
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
data:
|
||||
{{- (.Files.Glob "migrations/*.sql").AsConfig | nindent 2 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,62 @@
|
||||
{{- if and .Values.migrations.enabled .Values.postgres.enabled }}
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.fullname" . }}-migrations
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
helm.sh/hook: post-install,post-upgrade
|
||||
helm.sh/hook-weight: "5"
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
initContainers:
|
||||
- name: wait-postgres
|
||||
image: {{ .Values.images.busybox | quote }}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
until nc -z {{ include "cloudhost-platform.postgres.fullname" . }} 5432; do
|
||||
sleep 2
|
||||
done
|
||||
containers:
|
||||
- name: migrate
|
||||
image: {{ .Values.migrations.image | quote }}
|
||||
env:
|
||||
- name: PGHOST
|
||||
value: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
- name: PGPORT
|
||||
value: "5432"
|
||||
- name: PGDATABASE
|
||||
value: {{ .Values.postgres.database | quote }}
|
||||
- name: PGUSER
|
||||
value: {{ .Values.postgres.username | quote }}
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "cloudhost-platform.secretName" . }}
|
||||
key: postgres-password
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
for f in $(ls /migrations/*.sql | sort); do
|
||||
echo ">>> Applying $f"
|
||||
psql -v ON_ERROR_STOP=1 -f "$f"
|
||||
done
|
||||
echo ">>> All migrations applied"
|
||||
volumeMounts:
|
||||
- name: migrations
|
||||
mountPath: /migrations
|
||||
volumes:
|
||||
- name: migrations
|
||||
configMap:
|
||||
name: {{ include "cloudhost-platform.fullname" . }}-migrations
|
||||
{{- end }}
|
||||
@@ -0,0 +1,8 @@
|
||||
{{- if .Values.createNamespace }}
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,66 @@
|
||||
{{- if .Values.postgres.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: {{ .Values.images.postgres | quote }}
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
value: {{ .Values.postgres.database | quote }}
|
||||
- name: POSTGRES_USER
|
||||
value: {{ .Values.postgres.username | quote }}
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "cloudhost-platform.secretName" . }}
|
||||
key: postgres-password
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- {{ .Values.postgres.username }}
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- pg_isready
|
||||
- -U
|
||||
- {{ .Values.postgres.username }}
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
{{- with .Values.postgres.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "cloudhost-platform.postgres.fullname" . }}-data
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.postgres.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.postgres.fullname" . }}-data
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{- if .Values.global.storageClass }}
|
||||
storageClassName: {{ .Values.global.storageClass | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.postgres.storage | quote }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- if .Values.postgres.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: {{ include "cloudhost-platform.postgres.fullname" . }}
|
||||
ports:
|
||||
- name: postgres
|
||||
port: 5432
|
||||
targetPort: 5432
|
||||
{{- end }}
|
||||
@@ -0,0 +1,52 @@
|
||||
{{- if .Values.redis.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: {{ .Values.images.redis | quote }}
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- redis-cli
|
||||
- ping
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- redis-cli
|
||||
- ping
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
{{- with .Values.redis.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "cloudhost-platform.redis.fullname" . }}-data
|
||||
{{- end }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- if .Values.redis.enabled }}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.redis.fullname" . }}-data
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{- if .Values.global.storageClass }}
|
||||
storageClassName: {{ .Values.global.storageClass | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.redis.storage | quote }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,18 @@
|
||||
{{- if .Values.redis.enabled }}
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
app: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: {{ include "cloudhost-platform.redis.fullname" . }}
|
||||
ports:
|
||||
- name: redis
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
{{- end }}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{- $existing := lookup "v1" "Secret" (include "cloudhost-platform.namespace" .) (include "cloudhost-platform.secretName" .) }}
|
||||
{{- $pgPass := .Values.postgres.password }}
|
||||
{{- if not $pgPass }}
|
||||
{{- if $existing }}{{- $pgPass = index $existing.data "postgres-password" | b64dec }}{{- else }}{{- $pgPass = randAlphaNum 24 }}{{- end }}
|
||||
{{- end }}
|
||||
{{- $jwt := .Values.secrets.jwtSecret }}
|
||||
{{- if not $jwt }}
|
||||
{{- if $existing }}{{- $jwt = index $existing.data "jwt-secret" | b64dec }}{{- else }}{{- $jwt = randAlphaNum 32 }}{{- end }}
|
||||
{{- end }}
|
||||
{{- $jwtRefresh := .Values.secrets.jwtRefreshSecret }}
|
||||
{{- if not $jwtRefresh }}
|
||||
{{- if $existing }}{{- $jwtRefresh = index $existing.data "jwt-refresh-secret" | b64dec }}{{- else }}{{- $jwtRefresh = randAlphaNum 32 }}{{- end }}
|
||||
{{- end }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "cloudhost-platform.secretName" . }}
|
||||
namespace: {{ include "cloudhost-platform.namespace" . }}
|
||||
labels:
|
||||
{{- include "cloudhost-platform.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
postgres-password: {{ $pgPass | quote }}
|
||||
jwt-secret: {{ $jwt | quote }}
|
||||
jwt-refresh-secret: {{ $jwtRefresh | quote }}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated after local docker build — use with:
|
||||
# helm upgrade --install cloudhost . -n cloudhost -f values.yaml -f values-images.yaml
|
||||
#
|
||||
# Built with:
|
||||
# docker build -t cloudhost-backend:1.0.0 ./backend
|
||||
# docker build -t cloudhost-frontend:1.0.0 \
|
||||
# --build-arg NEXT_PUBLIC_API_URL=https://api.cloudhost.local ./frontend
|
||||
|
||||
images:
|
||||
backend:
|
||||
repository: cloudhost-backend
|
||||
tag: "1.0.0"
|
||||
pullPolicy: IfNotPresent
|
||||
frontend:
|
||||
repository: cloudhost-frontend
|
||||
tag: "1.0.0"
|
||||
pullPolicy: IfNotPresent
|
||||
@@ -0,0 +1,44 @@
|
||||
# Example production overrides for cloudhost-platform
|
||||
# cp values-production.example.yaml values-production.yaml && edit secrets/hosts
|
||||
|
||||
namespace: cloudhost
|
||||
createNamespace: true
|
||||
|
||||
global:
|
||||
storageClass: local-path # k3s example
|
||||
|
||||
images:
|
||||
backend:
|
||||
repository: registry.example.com/cloudhost-backend
|
||||
tag: "1.0.0"
|
||||
pullPolicy: Always
|
||||
frontend:
|
||||
repository: registry.example.com/cloudhost-frontend
|
||||
tag: "1.0.0"
|
||||
pullPolicy: Always
|
||||
|
||||
postgres:
|
||||
password: "CHANGE_ME_STRONG_POSTGRES_PASSWORD"
|
||||
|
||||
secrets:
|
||||
jwtSecret: "CHANGE_ME_LONG_JWT_SECRET"
|
||||
jwtRefreshSecret: "CHANGE_ME_LONG_REFRESH_SECRET"
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
frontend:
|
||||
host: platform.example.com
|
||||
api:
|
||||
host: api.platform.example.com
|
||||
tls:
|
||||
enabled: true
|
||||
clusterIssuer: letsencrypt-prod
|
||||
|
||||
backend:
|
||||
env:
|
||||
PLATFORM_DOMAIN: apps.example.com
|
||||
REGISTRY_PULL_URL: "10.0.0.50:30500"
|
||||
|
||||
migrations:
|
||||
enabled: true
|
||||
@@ -0,0 +1,93 @@
|
||||
# ────────────────────────────────────────────────────────────
|
||||
# CloudHost Platform — Helm values
|
||||
# Deploy: helm upgrade --install cloudhost ./backend/helm/cloudhost-platform -n cloudhost --create-namespace
|
||||
# ────────────────────────────────────────────────────────────
|
||||
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
namespace: cloudhost
|
||||
createNamespace: true
|
||||
|
||||
global:
|
||||
storageClass: ""
|
||||
|
||||
images:
|
||||
postgres: postgres:16-alpine
|
||||
redis: redis:7-alpine
|
||||
busybox: busybox:1.36
|
||||
backend:
|
||||
repository: cloudhost-backend
|
||||
tag: "1.0.0"
|
||||
pullPolicy: IfNotPresent
|
||||
frontend:
|
||||
repository: cloudhost-frontend
|
||||
tag: "1.0.0"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
postgres:
|
||||
enabled: true
|
||||
database: cloudhost
|
||||
username: cloudhost
|
||||
# Leave empty to auto-generate on first install (stored in Secret)
|
||||
password: ""
|
||||
storage: 10Gi
|
||||
resources: {}
|
||||
|
||||
redis:
|
||||
enabled: true
|
||||
storage: 1Gi
|
||||
resources: {}
|
||||
|
||||
backend:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
uploads:
|
||||
size: 20Gi
|
||||
resources: {}
|
||||
extraEnv: {}
|
||||
env:
|
||||
NODE_ENV: production
|
||||
PORT: "4000"
|
||||
JWT_EXPIRES_IN: 15m
|
||||
JWT_REFRESH_EXPIRES_IN: 7d
|
||||
PLATFORM_DOMAIN: apps.cloudhost.local
|
||||
REGISTRY_URL: registry.cloudhost-builds.svc.cluster.local:5000
|
||||
REGISTRY_PULL_URL: localhost:30500
|
||||
BUILD_NAMESPACE: cloudhost-builds
|
||||
BUILD_SERVICE_ACCOUNT: kaniko-builder
|
||||
UPLOAD_DIR: /app/uploads
|
||||
PLATFORM_CREATE_STORAGE_CLASS: "true"
|
||||
PLATFORM_STORAGE_CLASS: cloudhost-expandable
|
||||
PLATFORM_STORAGE_PROVISIONER: rancher.io/local-path
|
||||
|
||||
frontend:
|
||||
enabled: true
|
||||
replicas: 1
|
||||
resources: {}
|
||||
|
||||
# JWT secrets — set in production (values-production.example.yaml)
|
||||
secrets:
|
||||
jwtSecret: ""
|
||||
jwtRefreshSecret: ""
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
frontend:
|
||||
host: platform.cloudhost.local
|
||||
api:
|
||||
host: api.cloudhost.local
|
||||
singleHost:
|
||||
enabled: false
|
||||
host: platform.cloudhost.local
|
||||
apiPath: /api
|
||||
annotations: {}
|
||||
tls:
|
||||
enabled: true
|
||||
clusterIssuer: letsencrypt-prod
|
||||
secretName: ""
|
||||
|
||||
migrations:
|
||||
enabled: true
|
||||
image: postgres:16-alpine
|
||||
@@ -12,6 +12,8 @@ WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ARG NEXT_PUBLIC_API_URL=http://localhost:4000
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: admin@3fase.ir
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod-account-key
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: traefik
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: registry-public
|
||||
namespace: cloudhost-builds
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
traefik.ingress.kubernetes.io/service.serverstransport: cloudhost-builds-registry-transport@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts:
|
||||
- repo.3fase.ir
|
||||
secretName: repo-3fase-ir-tls
|
||||
rules:
|
||||
- host: repo.3fase.ir
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: registry
|
||||
port:
|
||||
number: 5000
|
||||
@@ -0,0 +1,22 @@
|
||||
# Patch registry Deployment for reverse-proxy (Traefik + HTTPS)
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: registry
|
||||
namespace: cloudhost-builds
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: registry
|
||||
env:
|
||||
- name: REGISTRY_STORAGE_DELETE_ENABLED
|
||||
value: "true"
|
||||
- name: REGISTRY_HTTP_RELATIVEURLS
|
||||
value: "true"
|
||||
- name: REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin
|
||||
value: '["*"]'
|
||||
- name: REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods
|
||||
value: '["HEAD","GET","OPTIONS","DELETE"]'
|
||||
- name: REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers
|
||||
value: '["Authorization","Accept","Cache-Control"]'
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: ServersTransport
|
||||
metadata:
|
||||
name: registry-transport
|
||||
namespace: cloudhost-builds
|
||||
spec:
|
||||
forwardingTimeouts:
|
||||
dialTimeout: 30s
|
||||
responseHeaderTimeout: 600s
|
||||
idleConnTimeout: 600s
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: registry-buffering
|
||||
namespace: cloudhost-builds
|
||||
spec:
|
||||
buffering:
|
||||
maxRequestBodyBytes: 0
|
||||
memRequestBodyBytes: 20971520
|
||||
maxResponseBodyBytes: 0
|
||||
memResponseBodyBytes: 20971520
|
||||
retryExpression: "IsNetworkError() && Attempts() < 2"
|
||||
@@ -0,0 +1,14 @@
|
||||
# Merge with existing traefik helm values (helm upgrade traefik -f traefik-timeout-values.yaml)
|
||||
ports:
|
||||
web:
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "0s"
|
||||
writeTimeout: "0s"
|
||||
idleTimeout: "1800s"
|
||||
websecure:
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "0s"
|
||||
writeTimeout: "0s"
|
||||
idleTimeout: "1800s"
|
||||
Reference in New Issue
Block a user