mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
* feat(self-host): align Docker Compose with Helm and overhaul self-hosting docs Docker Compose shipped no scheduler, so scheduled workflows, every polling trigger, connector syncs, the outbox, and data drains silently never ran. Adds a cron service running the same 18 jobs the Helm chart schedules as CronJobs, and closes the remaining behavioral gaps between the two paths: bundled Redis in the chart, no hosted plan caps in chart defaults, pinned image tags, and fail-fast secrets. A CI check keeps the schedulers in sync. Also rewrites the self-hosting docs: 14 new pages, 8 updated, reorganized into Install / Configure / Operate. * fix(self-host): drop bun install from chart CI, remove air-gapped and backup docs The scheduler-parity check pulled a full dependency install into the chart-validation job, which fails building isolated-vm on that runner. Rewritten to use only node builtins so the job installs nothing. Also removes the air-gapped and backup/restore pages, and stops pinning a concrete release in the docs so the examples do not go stale each release. * fix(helm): bundle Redis in secret-manager modes unless the URL is supplied Suppressing Redis whenever a secret mode was active left those deployments with no Redis at all — REDIS_URL is optional there and both shipped examples omit it. The chart now steps aside only on a detectable signal: an explicit app.env.REDIS_URL, an ESO remoteRefs.app.REDIS_URL mapping, or the new redis.provideUrl=false opt-out for a pre-created Secret it cannot read. * fix(compose): derive realtime BETTER_AUTH_URL from NEXT_PUBLIC_APP_URL realtime read BETTER_AUTH_URL directly and fell back to localhost while simstudio derived it from NEXT_PUBLIC_APP_URL, so setting only the public origin left realtime authenticating against http://localhost:3000. * fix(helm): deliver bundled REDIS_URL via ConfigMap so an operator value always wins Injecting REDIS_URL as an inline container env made it beat every envFrom source, so a REDIS_URL held in a pre-created Secret or synced by External Secrets was silently shadowed and traffic moved to a fresh in-cluster Redis. Kubernetes resolves duplicate envFrom keys by letting the last source win, so the bundled URL now ships as a ConfigMap listed before the app Secret. Any operator-supplied value overrides it without the chart needing to read it, which also removes the redis.provideUrl flag the previous attempt required. * docs(helm): spell out the egress rule external datastores need The default NetworkPolicy allows 443 plus the bundled Postgres and Redis by pod selector. Anything you run outside the chart on another port needs its own rule, which is easiest to miss when REDIS_URL arrives via a Secret the chart cannot inspect. Adds a copyable example to the production checklist and the security guide. * feat(helm): add networkPolicy.allowExternalEgress for managed datastores The default policy allows 443 plus the bundled Postgres and Redis by pod selector, so a managed datastore on another port needs a hand-written CIDR rule — awkward when REDIS_URL arrives via a Secret the chart cannot inspect. Adds an opt-in switch that drops the port restriction while still blocking the cloud metadata endpoints. Defaults to false, keeping this chart stricter than the common chart default of unrestricted egress.
1358 lines
45 KiB
JSON
1358 lines
45 KiB
JSON
{
|
|
"$schema": "https://json-schema.org/draft-07/schema#",
|
|
"type": "object",
|
|
"required": ["app", "realtime"],
|
|
"properties": {
|
|
"global": {
|
|
"type": "object",
|
|
"properties": {
|
|
"imageRegistry": {
|
|
"type": "string",
|
|
"description": "Global Docker image registry (host[:port][/path]). Supports plain hosts (registry.example.com), host:port (registry.example.com:5000), and host+path forms used by Artifactory virtual repos, Harbor projects, GCR (gcr.io/project-id), and ECR-with-namespace."
|
|
},
|
|
"useRegistryForAllImages": {
|
|
"type": "boolean",
|
|
"description": "Use registry for all images, not just simstudioai/* images"
|
|
},
|
|
"imagePullSecrets": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object"
|
|
},
|
|
"description": "Global image pull secrets"
|
|
},
|
|
"storageClass": {
|
|
"type": "string",
|
|
"description": "Global storage class for PVCs"
|
|
}
|
|
}
|
|
},
|
|
"app": {
|
|
"type": "object",
|
|
"required": ["enabled"],
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable the main application"
|
|
},
|
|
"replicaCount": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Number of app replicas"
|
|
},
|
|
"resources": {
|
|
"type": "object",
|
|
"properties": {
|
|
"limits": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory limit (e.g., 4Gi, 2048Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU limit (e.g., 2000m, 2.0)"
|
|
},
|
|
"nvidia.com/gpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+$",
|
|
"description": "GPU limit"
|
|
}
|
|
}
|
|
},
|
|
"requests": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory request (e.g., 2Gi, 1024Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU request (e.g., 1000m, 1.0)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"secrets": {
|
|
"type": "object",
|
|
"description": "Secret management configuration",
|
|
"properties": {
|
|
"existingSecret": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Use an existing secret instead of creating one"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Name of the existing Kubernetes secret"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"env": {
|
|
"type": "object",
|
|
"properties": {
|
|
"BETTER_AUTH_SECRET": {
|
|
"type": "string",
|
|
"anyOf": [{ "minLength": 32 }, { "const": "" }],
|
|
"description": "Auth secret (minimum 32 characters required when not using existingSecret; leave empty when the value comes from existingSecret/ESO instead)"
|
|
},
|
|
"ENCRYPTION_KEY": {
|
|
"type": "string",
|
|
"anyOf": [{ "minLength": 32 }, { "const": "" }],
|
|
"description": "Encryption key (minimum 32 characters required when not using existingSecret; leave empty when the value comes from existingSecret/ESO instead)"
|
|
},
|
|
"NEXT_PUBLIC_APP_URL": {
|
|
"type": "string",
|
|
"anyOf": [{ "format": "uri" }, { "const": "" }],
|
|
"description": "Public application URL (default http://localhost:3000 via envDefaults; set explicitly here to override in Secret)"
|
|
},
|
|
"INTERNAL_API_BASE_URL": {
|
|
"type": "string",
|
|
"anyOf": [
|
|
{
|
|
"format": "uri"
|
|
},
|
|
{
|
|
"const": ""
|
|
}
|
|
],
|
|
"description": "Optional server-side internal base URL for internal /api self-calls (must include http:// or https://); defaults to NEXT_PUBLIC_APP_URL when unset"
|
|
},
|
|
"BETTER_AUTH_URL": {
|
|
"type": "string",
|
|
"anyOf": [{ "format": "uri" }, { "const": "" }],
|
|
"description": "Authentication service URL (default http://localhost:3000 via envDefaults; set explicitly here to override in Secret)"
|
|
},
|
|
"SOCKET_SERVER_URL": {
|
|
"type": "string",
|
|
"format": "uri",
|
|
"description": "Socket server URL"
|
|
},
|
|
"NEXT_PUBLIC_SOCKET_URL": {
|
|
"type": "string",
|
|
"anyOf": [
|
|
{
|
|
"format": "uri"
|
|
},
|
|
{
|
|
"const": ""
|
|
}
|
|
],
|
|
"description": "Public socket URL; leave empty to default to the page origin (assumes reverse proxy routes /socket.io)"
|
|
},
|
|
"TRUSTED_ORIGINS": {
|
|
"type": "string",
|
|
"description": "Comma-separated additional public origins to trust for auth (e.g. 'https://app.example.com,https://www.example.com'). Merged into Better Auth trustedOrigins."
|
|
},
|
|
"AUTH_TRUSTED_PROXIES": {
|
|
"type": "string",
|
|
"description": "Comma-separated reverse-proxy IPs or CIDR ranges in front of the app (e.g. '10.0.0.0/16'). Better Auth walks x-forwarded-for right to left, skips these hops, and uses the first untrusted address as the client IP."
|
|
},
|
|
"SSO_TRUSTED_PROVIDER_IDS": {
|
|
"type": "string",
|
|
"description": "Comma-separated SSO provider IDs to trust for automatic account linking when an SSO sign-in matches an existing account's email. Only needed for IdPs that do not assert email_verified. Merged into Better Auth accountLinking.trustedProviders."
|
|
},
|
|
"NODE_ENV": {
|
|
"type": "string",
|
|
"enum": ["development", "test", "production"],
|
|
"description": "Runtime environment"
|
|
},
|
|
"NEXT_TELEMETRY_DISABLED": {
|
|
"type": "string",
|
|
"description": "Disable Next.js telemetry"
|
|
},
|
|
"RESEND_API_KEY": {
|
|
"type": "string",
|
|
"description": "Resend API key for transactional emails"
|
|
},
|
|
"FROM_EMAIL_ADDRESS": {
|
|
"type": "string",
|
|
"description": "Complete from address (e.g., \"Sim <noreply@domain.com>\" or \"DoNotReply@domain.com\")"
|
|
},
|
|
"EMAIL_DOMAIN": {
|
|
"type": "string",
|
|
"description": "Domain for sending emails (fallback when FROM_EMAIL_ADDRESS not set)"
|
|
},
|
|
"AWS_SES_REGION": {
|
|
"type": "string",
|
|
"description": "AWS region for SES (e.g., 'us-east-1'). Credentials are resolved via the standard AWS provider chain (env vars, IRSA, EC2/ECS task role, SSO)."
|
|
},
|
|
"AZURE_ACS_CONNECTION_STRING": {
|
|
"type": "string",
|
|
"description": "Azure Communication Services connection string (email provider — used when Resend/SES/SMTP are not configured)."
|
|
},
|
|
"SMTP_HOST": {
|
|
"type": "string",
|
|
"description": "SMTP server hostname. When set together with SMTP_PORT, enables the SMTP mail provider."
|
|
},
|
|
"SMTP_PORT": {
|
|
"type": "string",
|
|
"description": "SMTP server port. 465 = implicit TLS, 587 = STARTTLS, 25 = plain."
|
|
},
|
|
"SMTP_USER": {
|
|
"type": "string",
|
|
"description": "SMTP username (optional — leave empty for unauthenticated relays like MailHog)."
|
|
},
|
|
"SMTP_PASS": {
|
|
"type": "string",
|
|
"description": "SMTP password (optional — leave empty for unauthenticated relays)."
|
|
},
|
|
"SMTP_SECURE": {
|
|
"type": "string",
|
|
"description": "Set to 'true' to force TLS on connect. Defaults to true when SMTP_PORT=465."
|
|
},
|
|
"GMAIL_CREDENTIALS_JSON": {
|
|
"type": "string",
|
|
"description": "Inline Google service-account JSON with domain-wide delegation for the Gmail API mail provider."
|
|
},
|
|
"GMAIL_SENDER": {
|
|
"type": "string",
|
|
"description": "Google Workspace user the Gmail service account impersonates when sending (e.g., noreply@yourdomain.com)."
|
|
},
|
|
"GOOGLE_CLIENT_ID": {
|
|
"type": "string",
|
|
"description": "Google OAuth client ID"
|
|
},
|
|
"GOOGLE_CLIENT_SECRET": {
|
|
"type": "string",
|
|
"description": "Google OAuth client secret"
|
|
},
|
|
"GITHUB_CLIENT_ID": {
|
|
"type": "string",
|
|
"description": "GitHub OAuth client ID"
|
|
},
|
|
"GITHUB_CLIENT_SECRET": {
|
|
"type": "string",
|
|
"description": "GitHub OAuth client secret"
|
|
},
|
|
"DISABLE_GOOGLE_AUTH": {
|
|
"type": "string",
|
|
"description": "Set to 'true' to hide Google OAuth login even when credentials are configured"
|
|
},
|
|
"DISABLE_GITHUB_AUTH": {
|
|
"type": "string",
|
|
"description": "Set to 'true' to hide GitHub OAuth login even when credentials are configured"
|
|
},
|
|
"OPENAI_API_KEY": {
|
|
"type": "string",
|
|
"description": "Primary OpenAI API key"
|
|
},
|
|
"OPENAI_API_KEY_1": {
|
|
"type": "string",
|
|
"description": "Additional OpenAI API key for load balancing"
|
|
},
|
|
"OPENAI_API_KEY_2": {
|
|
"type": "string",
|
|
"description": "Additional OpenAI API key for load balancing"
|
|
},
|
|
"OPENAI_API_KEY_3": {
|
|
"type": "string",
|
|
"description": "Additional OpenAI API key for load balancing"
|
|
},
|
|
"MISTRAL_API_KEY": {
|
|
"type": "string",
|
|
"description": "Mistral AI API key"
|
|
},
|
|
"ANTHROPIC_API_KEY_1": {
|
|
"type": "string",
|
|
"description": "Primary Anthropic Claude API key"
|
|
},
|
|
"ANTHROPIC_API_KEY_2": {
|
|
"type": "string",
|
|
"description": "Additional Anthropic API key for load balancing"
|
|
},
|
|
"ANTHROPIC_API_KEY_3": {
|
|
"type": "string",
|
|
"description": "Additional Anthropic API key for load balancing"
|
|
},
|
|
"OLLAMA_URL": {
|
|
"type": "string",
|
|
"description": "Ollama local LLM server URL (leave empty if not using Ollama)"
|
|
},
|
|
"ELEVENLABS_API_KEY": {
|
|
"type": "string",
|
|
"description": "ElevenLabs API key for text-to-speech in deployed chat"
|
|
},
|
|
"KB_OPENAI_MODEL_NAME": {
|
|
"type": "string",
|
|
"description": "Azure deployment name serving the configured KB embedding model (used only when AZURE_OPENAI_* credentials are set)."
|
|
},
|
|
"WAND_OPENAI_MODEL_NAME": {
|
|
"type": "string",
|
|
"description": "Wand generation model deployment name (works with both regular OpenAI and Azure OpenAI)."
|
|
},
|
|
"OCR_AZURE_ENDPOINT": {
|
|
"type": "string",
|
|
"description": "Azure Mistral OCR service endpoint."
|
|
},
|
|
"OCR_AZURE_MODEL_NAME": {
|
|
"type": "string",
|
|
"description": "Azure Mistral OCR model name."
|
|
},
|
|
"OCR_AZURE_API_KEY": {
|
|
"type": "string",
|
|
"description": "Azure Mistral OCR API key."
|
|
},
|
|
"RATE_LIMIT_WINDOW_MS": {
|
|
"type": "string",
|
|
"description": "Rate limit window duration in milliseconds"
|
|
},
|
|
"RATE_LIMIT_FREE_SYNC": {
|
|
"type": "string",
|
|
"description": "Free tier sync API executions per minute"
|
|
},
|
|
"RATE_LIMIT_PRO_SYNC": {
|
|
"type": "string",
|
|
"description": "Pro tier sync API executions per minute"
|
|
},
|
|
"RATE_LIMIT_TEAM_SYNC": {
|
|
"type": "string",
|
|
"description": "Team tier sync API executions per minute"
|
|
},
|
|
"RATE_LIMIT_ENTERPRISE_SYNC": {
|
|
"type": "string",
|
|
"description": "Enterprise tier sync API executions per minute"
|
|
},
|
|
"RATE_LIMIT_FREE_ASYNC": {
|
|
"type": "string",
|
|
"description": "Free tier async API executions per minute"
|
|
},
|
|
"RATE_LIMIT_FREE_API_ENDPOINT": {
|
|
"type": "string",
|
|
"description": "Free tier v1 API endpoint requests per minute"
|
|
},
|
|
"RATE_LIMIT_PRO_ASYNC": {
|
|
"type": "string",
|
|
"description": "Pro tier async API executions per minute"
|
|
},
|
|
"RATE_LIMIT_TEAM_ASYNC": {
|
|
"type": "string",
|
|
"description": "Team tier async API executions per minute"
|
|
},
|
|
"RATE_LIMIT_ENTERPRISE_ASYNC": {
|
|
"type": "string",
|
|
"description": "Enterprise tier async API executions per minute"
|
|
},
|
|
"BILLING_CONCURRENCY_LIMIT_FREE": {
|
|
"type": "string",
|
|
"description": "In-flight executions per free billing account"
|
|
},
|
|
"BILLING_CONCURRENCY_LIMIT_PRO": {
|
|
"type": "string",
|
|
"description": "In-flight executions per Pro-tier billing account (Pro and Pro for Teams)"
|
|
},
|
|
"BILLING_CONCURRENCY_LIMIT_TEAM": {
|
|
"type": "string",
|
|
"description": "In-flight executions per Max-tier billing account (Max and Max for Teams)"
|
|
},
|
|
"BILLING_CONCURRENCY_LIMIT_ENTERPRISE": {
|
|
"type": "string",
|
|
"description": "Default in-flight executions per Enterprise billing account"
|
|
},
|
|
"MANUAL_EXECUTION_LIMIT": {
|
|
"type": "string",
|
|
"description": "Manual execution bypass value"
|
|
},
|
|
"NEXT_PUBLIC_BRAND_NAME": {
|
|
"type": "string",
|
|
"description": "Custom brand name"
|
|
},
|
|
"NEXT_PUBLIC_BRAND_LOGO_URL": {
|
|
"type": "string",
|
|
"description": "Custom logo URL (leave empty for default)"
|
|
},
|
|
"NEXT_PUBLIC_BRAND_FAVICON_URL": {
|
|
"type": "string",
|
|
"description": "Custom favicon URL (leave empty for default)"
|
|
},
|
|
"NEXT_PUBLIC_CUSTOM_CSS_URL": {
|
|
"type": "string",
|
|
"description": "Custom stylesheet URL (leave empty for none)"
|
|
},
|
|
"NEXT_PUBLIC_SUPPORT_EMAIL": {
|
|
"type": "string",
|
|
"anyOf": [{ "format": "email" }, { "const": "" }],
|
|
"description": "Support email address (default help@sim.ai via envDefaults; set explicitly here to override in Secret)"
|
|
},
|
|
"NEXT_PUBLIC_DOCUMENTATION_URL": {
|
|
"type": "string",
|
|
"description": "Documentation URL"
|
|
},
|
|
"NEXT_PUBLIC_TERMS_URL": {
|
|
"type": "string",
|
|
"description": "Terms of service URL"
|
|
},
|
|
"NEXT_PUBLIC_PRIVACY_URL": {
|
|
"type": "string",
|
|
"description": "Privacy policy URL"
|
|
},
|
|
"ALLOWED_LOGIN_EMAILS": {
|
|
"type": "string",
|
|
"description": "Comma-separated list of allowed email addresses for login"
|
|
},
|
|
"ALLOWED_LOGIN_DOMAINS": {
|
|
"type": "string",
|
|
"description": "Comma-separated list of allowed email domains for login"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"realtime": {
|
|
"type": "object",
|
|
"required": ["enabled"],
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable the realtime service"
|
|
},
|
|
"replicaCount": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Number of realtime replicas"
|
|
},
|
|
"resources": {
|
|
"type": "object",
|
|
"properties": {
|
|
"limits": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory limit (e.g., 2Gi, 1024Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU limit (e.g., 1000m, 1.0)"
|
|
}
|
|
}
|
|
},
|
|
"requests": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory request (e.g., 1Gi, 512Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU request (e.g., 500m, 0.5)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"env": {
|
|
"type": "object",
|
|
"properties": {
|
|
"BETTER_AUTH_SECRET": {
|
|
"type": "string",
|
|
"anyOf": [{ "minLength": 32 }, { "const": "" }],
|
|
"description": "Auth secret (minimum 32 characters required when not using existingSecret; leave empty when the value comes from existingSecret/ESO instead)"
|
|
},
|
|
"NEXT_PUBLIC_APP_URL": {
|
|
"type": "string",
|
|
"anyOf": [{ "format": "uri" }, { "const": "" }],
|
|
"description": "Public application URL (default http://localhost:3000 via envDefaults; set explicitly here to override in Secret)"
|
|
},
|
|
"BETTER_AUTH_URL": {
|
|
"type": "string",
|
|
"anyOf": [{ "format": "uri" }, { "const": "" }],
|
|
"description": "Authentication service URL (default http://localhost:3000 via envDefaults; set explicitly here to override in Secret)"
|
|
},
|
|
"ALLOWED_ORIGINS": {
|
|
"type": "string",
|
|
"description": "CORS allowed origins"
|
|
},
|
|
"NODE_ENV": {
|
|
"type": "string",
|
|
"enum": ["development", "test", "production"],
|
|
"description": "Runtime environment"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"redis": {
|
|
"type": "object",
|
|
"description": "In-cluster Redis for pub/sub, the Socket.IO adapter, and the idempotency/progress stores. Disable and set app.env.REDIS_URL to use a managed instance.",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Deploy the bundled Redis. Skipped automatically when app.env.REDIS_URL is set or a secret-manager mode is active."
|
|
},
|
|
"image": {
|
|
"type": "object",
|
|
"properties": {
|
|
"repository": { "type": "string" },
|
|
"tag": { "type": "string" },
|
|
"digest": { "type": "string" },
|
|
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
|
|
}
|
|
},
|
|
"maxmemory": { "type": "string", "description": "Redis maxmemory value, e.g. 512mb" },
|
|
"maxmemoryPolicy": {
|
|
"type": "string",
|
|
"enum": [
|
|
"noeviction",
|
|
"allkeys-lru",
|
|
"allkeys-lfu",
|
|
"allkeys-random",
|
|
"volatile-lru",
|
|
"volatile-lfu",
|
|
"volatile-random",
|
|
"volatile-ttl"
|
|
],
|
|
"description": "Redis eviction policy. An invalid value makes Redis exit at startup, so it is enumerated."
|
|
},
|
|
"resources": { "type": "object" },
|
|
"nodeSelector": { "type": "object" },
|
|
"podSecurityContext": { "type": "object" },
|
|
"securityContext": { "type": "object" }
|
|
}
|
|
},
|
|
"postgresql": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable internal PostgreSQL"
|
|
},
|
|
"resources": {
|
|
"type": "object",
|
|
"properties": {
|
|
"limits": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory limit (e.g., 2Gi, 1024Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU limit"
|
|
}
|
|
}
|
|
},
|
|
"requests": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory request (e.g., 1Gi, 512Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU request (e.g., 500m, 0.5)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"persistence": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable persistent storage"
|
|
},
|
|
"size": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei)$",
|
|
"description": "Storage size (e.g., 10Gi, 50Gi)"
|
|
}
|
|
}
|
|
},
|
|
"auth": {
|
|
"type": "object",
|
|
"properties": {
|
|
"username": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "PostgreSQL username"
|
|
},
|
|
"database": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "PostgreSQL database name"
|
|
},
|
|
"password": {
|
|
"type": "string",
|
|
"anyOf": [{ "minLength": 8 }, { "const": "" }],
|
|
"description": "PostgreSQL password (minimum 8 characters when not using existingSecret; leave empty when the value comes from existingSecret/ESO instead)"
|
|
},
|
|
"existingSecret": {
|
|
"type": "object",
|
|
"description": "Use an existing secret for PostgreSQL credentials",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Use an existing secret instead of creating one"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Name of the existing Kubernetes secret"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"externalDatabase": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Use external database"
|
|
},
|
|
"host": {
|
|
"type": "string",
|
|
"format": "hostname",
|
|
"description": "External database host"
|
|
},
|
|
"port": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"maximum": 65535,
|
|
"description": "External database port"
|
|
},
|
|
"username": {
|
|
"type": "string",
|
|
"description": "External database username"
|
|
},
|
|
"password": {
|
|
"type": "string",
|
|
"description": "External database password"
|
|
},
|
|
"database": {
|
|
"type": "string",
|
|
"description": "External database name"
|
|
},
|
|
"sslMode": {
|
|
"type": "string",
|
|
"enum": ["disable", "allow", "prefer", "require", "verify-ca", "verify-full"],
|
|
"description": "SSL mode for database connection"
|
|
},
|
|
"existingSecret": {
|
|
"type": "object",
|
|
"description": "Use an existing secret for external database credentials",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Use an existing secret instead of creating one"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Name of the existing Kubernetes secret"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"if": {
|
|
"properties": {
|
|
"enabled": {
|
|
"const": true
|
|
}
|
|
}
|
|
},
|
|
"then": {
|
|
"required": ["host", "username", "password", "database"]
|
|
}
|
|
},
|
|
"autoscaling": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable autoscaling"
|
|
},
|
|
"minReplicas": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Minimum replicas"
|
|
},
|
|
"maxReplicas": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Maximum replicas"
|
|
}
|
|
},
|
|
"if": {
|
|
"properties": {
|
|
"enabled": {
|
|
"const": true
|
|
}
|
|
}
|
|
},
|
|
"then": {
|
|
"required": ["minReplicas", "maxReplicas"]
|
|
}
|
|
},
|
|
"ollama": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable Ollama service"
|
|
},
|
|
"replicaCount": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Number of Ollama replicas"
|
|
},
|
|
"resources": {
|
|
"type": "object",
|
|
"properties": {
|
|
"limits": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory limit (e.g., 8Gi, 4096Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU limit"
|
|
},
|
|
"nvidia.com/gpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+$",
|
|
"description": "GPU limit"
|
|
}
|
|
}
|
|
},
|
|
"requests": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory request (e.g., 4Gi, 2048Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU request (e.g., 1000m, 1.0)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"persistence": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable persistent storage"
|
|
},
|
|
"size": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei)$",
|
|
"description": "Storage size (e.g., 100Gi, 500Gi)"
|
|
}
|
|
}
|
|
},
|
|
"gpu": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable GPU support"
|
|
},
|
|
"count": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Number of GPUs"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"pii": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable the Presidio PII redaction service"
|
|
},
|
|
"replicaCount": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Number of PII service replicas"
|
|
},
|
|
"image": {
|
|
"type": "object",
|
|
"properties": {
|
|
"repository": { "type": "string" },
|
|
"tag": { "type": "string" },
|
|
"digest": { "type": "string" },
|
|
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] }
|
|
}
|
|
},
|
|
"resources": {
|
|
"type": "object",
|
|
"properties": {
|
|
"limits": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory limit (e.g., 8Gi, 4096Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU limit"
|
|
}
|
|
}
|
|
},
|
|
"requests": {
|
|
"type": "object",
|
|
"properties": {
|
|
"memory": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(Ki|Mi|Gi|Ti|Pi|Ei|m|k|M|G|T|P|E)?$",
|
|
"description": "Memory request (e.g., 4Gi, 2048Mi)"
|
|
},
|
|
"cpu": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+(\\.[0-9]+)?m?$",
|
|
"description": "CPU request (e.g., 1000m, 1.0)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"service": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": { "type": "string" },
|
|
"port": { "type": "integer" },
|
|
"targetPort": { "type": "integer" }
|
|
}
|
|
},
|
|
"env": { "type": "object" },
|
|
"nodeSelector": { "type": "object" },
|
|
"topologySpreadConstraints": { "type": "array", "items": { "type": "object" } },
|
|
"podSecurityContext": { "type": "object" },
|
|
"securityContext": { "type": "object" },
|
|
"startupProbe": { "type": "object" },
|
|
"livenessProbe": { "type": "object" },
|
|
"readinessProbe": { "type": "object" },
|
|
"extraVolumes": { "type": "array", "items": { "type": "object" } },
|
|
"extraVolumeMounts": { "type": "array", "items": { "type": "object" } }
|
|
}
|
|
},
|
|
"telemetry": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable telemetry collection"
|
|
},
|
|
"replicaCount": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Number of telemetry collector replicas"
|
|
}
|
|
}
|
|
},
|
|
"tests": {
|
|
"type": "object",
|
|
"description": "Helm test hook configuration (helm test)",
|
|
"properties": {
|
|
"enabled": { "type": "boolean" },
|
|
"image": {
|
|
"type": "object",
|
|
"properties": {
|
|
"repository": { "type": "string" },
|
|
"tag": { "type": "string" },
|
|
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] },
|
|
"pullSecrets": { "type": "array", "items": { "type": "object" } }
|
|
}
|
|
},
|
|
"timeoutSeconds": { "type": "integer", "minimum": 1 },
|
|
"resources": { "type": "object" }
|
|
}
|
|
},
|
|
"sharedStorage": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable shared storage"
|
|
},
|
|
"volumes": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"required": ["name", "size"],
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "Volume name"
|
|
},
|
|
"size": {
|
|
"type": "string",
|
|
"pattern": "^[0-9]+[KMGT]i$",
|
|
"description": "Volume size (e.g., 100Gi)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"copilot": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable the Copilot microservice"
|
|
},
|
|
"server": {
|
|
"type": "object",
|
|
"properties": {
|
|
"replicaCount": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "Number of Copilot replicas"
|
|
},
|
|
"image": {
|
|
"type": "object",
|
|
"properties": {
|
|
"repository": {
|
|
"type": "string",
|
|
"description": "Copilot image repository"
|
|
},
|
|
"tag": {
|
|
"type": "string",
|
|
"description": "Copilot image tag"
|
|
},
|
|
"pullPolicy": {
|
|
"type": "string",
|
|
"enum": ["Always", "IfNotPresent", "Never"],
|
|
"description": "Image pull policy"
|
|
}
|
|
}
|
|
},
|
|
"resources": {
|
|
"type": "object",
|
|
"properties": {
|
|
"limits": { "type": "object" },
|
|
"requests": { "type": "object" }
|
|
}
|
|
},
|
|
"nodeSelector": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" }
|
|
},
|
|
"env": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" },
|
|
"description": "Environment variables for Copilot"
|
|
},
|
|
"extraEnv": {
|
|
"type": "array",
|
|
"items": { "type": "object" },
|
|
"description": "Additional environment variable definitions"
|
|
},
|
|
"extraEnvFrom": {
|
|
"type": "array",
|
|
"items": { "type": "object" },
|
|
"description": "Additional envFrom sources"
|
|
},
|
|
"secret": {
|
|
"type": "object",
|
|
"properties": {
|
|
"create": {
|
|
"type": "boolean",
|
|
"description": "Whether to create a secret from copilot.server.env"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Override name for the Copilot secret"
|
|
},
|
|
"annotations": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" },
|
|
"description": "Annotations added to the Copilot secret"
|
|
}
|
|
}
|
|
},
|
|
"service": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": { "type": "string" },
|
|
"port": { "type": "integer" },
|
|
"targetPort": { "type": "integer" }
|
|
}
|
|
},
|
|
"podDisruptionBudget": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable PodDisruptionBudget for Copilot server"
|
|
},
|
|
"minAvailable": {
|
|
"type": "integer",
|
|
"description": "Minimum number of available pods"
|
|
},
|
|
"maxUnavailable": {
|
|
"type": "integer",
|
|
"description": "Maximum number of unavailable pods"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"postgresql": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Deploy an internal PostgreSQL instance for Copilot"
|
|
},
|
|
"auth": {
|
|
"type": "object",
|
|
"properties": {
|
|
"username": { "type": "string" },
|
|
"password": { "type": "string" },
|
|
"database": { "type": "string" }
|
|
}
|
|
},
|
|
"persistence": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": { "type": "boolean" },
|
|
"size": { "type": "string" },
|
|
"storageClass": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"database": {
|
|
"type": "object",
|
|
"properties": {
|
|
"existingSecretName": {
|
|
"type": "string",
|
|
"description": "Existing secret containing the Copilot DATABASE_URL"
|
|
},
|
|
"secretKey": {
|
|
"type": "string",
|
|
"description": "Key name inside the database secret"
|
|
},
|
|
"url": {
|
|
"type": "string",
|
|
"description": "External database connection string"
|
|
}
|
|
}
|
|
},
|
|
"migrations": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable the Copilot migration job"
|
|
},
|
|
"image": {
|
|
"type": "object",
|
|
"properties": {
|
|
"repository": { "type": "string" },
|
|
"tag": { "type": "string" },
|
|
"pullPolicy": { "type": "string" }
|
|
}
|
|
},
|
|
"resources": {
|
|
"type": "object",
|
|
"properties": {
|
|
"limits": { "type": "object" },
|
|
"requests": { "type": "object" }
|
|
}
|
|
},
|
|
"backoffLimit": {
|
|
"type": "integer",
|
|
"minimum": 0
|
|
},
|
|
"restartPolicy": {
|
|
"type": "string",
|
|
"enum": ["Never", "OnFailure"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"externalSecrets": {
|
|
"type": "object",
|
|
"description": "External Secrets Operator integration",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable External Secrets Operator integration"
|
|
},
|
|
"apiVersion": {
|
|
"type": "string",
|
|
"enum": ["v1", "v1beta1"],
|
|
"description": "ESO API version - use v1 for ESO v0.17+ (recommended), v1beta1 for older versions"
|
|
},
|
|
"refreshInterval": {
|
|
"type": "string",
|
|
"description": "How often to sync secrets from external store"
|
|
},
|
|
"secretStoreRef": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Name of the SecretStore or ClusterSecretStore"
|
|
},
|
|
"kind": {
|
|
"type": "string",
|
|
"enum": ["SecretStore", "ClusterSecretStore"],
|
|
"description": "Kind of the store"
|
|
}
|
|
}
|
|
},
|
|
"remoteRefs": {
|
|
"type": "object",
|
|
"description": "Remote key paths in external secret store",
|
|
"properties": {
|
|
"app": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" }
|
|
},
|
|
"postgresql": {
|
|
"type": "object",
|
|
"properties": {
|
|
"password": { "type": "string" }
|
|
}
|
|
},
|
|
"externalDatabase": {
|
|
"type": "object",
|
|
"properties": {
|
|
"password": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"ingress": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable ingress"
|
|
},
|
|
"className": {
|
|
"type": "string",
|
|
"description": "Ingress class name"
|
|
},
|
|
"app": {
|
|
"type": "object",
|
|
"properties": {
|
|
"host": {
|
|
"type": "string",
|
|
"format": "hostname",
|
|
"description": "Main application hostname"
|
|
}
|
|
}
|
|
},
|
|
"realtime": {
|
|
"type": "object",
|
|
"properties": {
|
|
"host": {
|
|
"type": "string",
|
|
"format": "hostname",
|
|
"description": "Realtime service hostname"
|
|
}
|
|
}
|
|
},
|
|
"copilot": {
|
|
"type": "object",
|
|
"properties": {
|
|
"host": {
|
|
"type": "string",
|
|
"format": "hostname",
|
|
"description": "Copilot service hostname"
|
|
},
|
|
"paths": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": { "type": "string" },
|
|
"pathType": { "type": "string" }
|
|
}
|
|
},
|
|
"description": "Ingress paths for Copilot service"
|
|
}
|
|
}
|
|
},
|
|
"tls": {
|
|
"type": "object",
|
|
"properties": {
|
|
"enabled": {
|
|
"type": "boolean",
|
|
"description": "Enable TLS"
|
|
},
|
|
"secretName": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"description": "TLS secret name"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"nameOverride": {
|
|
"type": "string",
|
|
"description": "Override the chart name used in resource names"
|
|
},
|
|
"fullnameOverride": {
|
|
"type": "string",
|
|
"description": "Override the fully qualified release name used in resource names"
|
|
},
|
|
"affinity": {
|
|
"type": "object",
|
|
"description": "Pod affinity/anti-affinity rules applied to app and realtime pods"
|
|
},
|
|
"branding": {
|
|
"type": "object",
|
|
"description": "Whitelabel branding configuration (logos, names, colors)"
|
|
},
|
|
"certManager": {
|
|
"type": "object",
|
|
"description": "Chart-managed cert-manager issuers (self-signed bootstrap + CA) for postgresql.tls"
|
|
},
|
|
"cronjobs": {
|
|
"type": "object",
|
|
"description": "Scheduled jobs (schedule execution, polling, reconciliation)"
|
|
},
|
|
"extraEnvVars": {
|
|
"type": "array",
|
|
"description": "Additional environment variables added to app and realtime containers"
|
|
},
|
|
"extraVolumeMounts": {
|
|
"type": "array",
|
|
"description": "Additional volume mounts added to app and realtime containers"
|
|
},
|
|
"extraVolumes": {
|
|
"type": "array",
|
|
"description": "Additional volumes added to app and realtime pods"
|
|
},
|
|
"ingressInternal": {
|
|
"type": "object",
|
|
"description": "Secondary internal ingress configuration"
|
|
},
|
|
"migrations": {
|
|
"type": "object",
|
|
"description": "Database migrations init-container configuration"
|
|
},
|
|
"monitoring": {
|
|
"type": "object",
|
|
"description": "Prometheus Operator ServiceMonitor configuration"
|
|
},
|
|
"networkPolicy": {
|
|
"type": "object",
|
|
"description": "NetworkPolicy toggles and custom ingress/egress rules"
|
|
},
|
|
"podAnnotations": {
|
|
"type": "object",
|
|
"description": "Annotations added to app and realtime pods"
|
|
},
|
|
"podDisruptionBudget": {
|
|
"type": "object",
|
|
"description": "PodDisruptionBudget configuration for app and realtime"
|
|
},
|
|
"podLabels": {
|
|
"type": "object",
|
|
"description": "Labels added to app and realtime pods"
|
|
},
|
|
"serviceAccount": {
|
|
"type": "object",
|
|
"description": "ServiceAccount creation, name, and annotations (e.g. Workload Identity)"
|
|
},
|
|
"tolerations": {
|
|
"type": "array",
|
|
"description": "Tolerations applied to app and realtime pods"
|
|
}
|
|
},
|
|
"additionalProperties": false,
|
|
"allOf": [
|
|
{
|
|
"if": {
|
|
"properties": {
|
|
"postgresql": {
|
|
"properties": {
|
|
"enabled": {
|
|
"const": false
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"anyOf": [
|
|
{
|
|
"properties": {
|
|
"app": {
|
|
"properties": {
|
|
"enabled": {
|
|
"const": true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"properties": {
|
|
"realtime": {
|
|
"properties": {
|
|
"enabled": {
|
|
"const": true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"then": {
|
|
"properties": {
|
|
"externalDatabase": {
|
|
"properties": {
|
|
"enabled": {
|
|
"const": true
|
|
}
|
|
},
|
|
"required": ["enabled"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|