container lifecycle management
This commit is contained in:
327
gateway/src/k8s/client.ts
Normal file
327
gateway/src/k8s/client.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import * as k8s from '@kubernetes/client-node';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export interface K8sClientConfig {
|
||||
namespace: string;
|
||||
inCluster: boolean;
|
||||
context?: string; // For local dev
|
||||
logger: FastifyBaseLogger;
|
||||
}
|
||||
|
||||
export interface DeploymentSpec {
|
||||
userId: string;
|
||||
licenseType: 'free' | 'pro' | 'enterprise';
|
||||
agentImage: string;
|
||||
sidecarImage: string;
|
||||
storageClass: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kubernetes client wrapper for managing agent deployments
|
||||
*/
|
||||
export class KubernetesClient {
|
||||
private config: K8sClientConfig;
|
||||
private k8sConfig: k8s.KubeConfig;
|
||||
private appsApi: k8s.AppsV1Api;
|
||||
private coreApi: k8s.CoreV1Api;
|
||||
|
||||
constructor(config: K8sClientConfig) {
|
||||
this.config = config;
|
||||
this.k8sConfig = new k8s.KubeConfig();
|
||||
|
||||
if (config.inCluster) {
|
||||
this.k8sConfig.loadFromCluster();
|
||||
this.config.logger.info('Loaded in-cluster Kubernetes config');
|
||||
} else {
|
||||
this.k8sConfig.loadFromDefault();
|
||||
if (config.context) {
|
||||
this.k8sConfig.setCurrentContext(config.context);
|
||||
this.config.logger.info({ context: config.context }, 'Set Kubernetes context');
|
||||
}
|
||||
this.config.logger.info('Loaded Kubernetes config from default location');
|
||||
}
|
||||
|
||||
this.appsApi = this.k8sConfig.makeApiClient(k8s.AppsV1Api);
|
||||
this.coreApi = this.k8sConfig.makeApiClient(k8s.CoreV1Api);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate deployment name from user ID
|
||||
*/
|
||||
static getDeploymentName(userId: string): string {
|
||||
// Sanitize userId to be k8s-compliant (lowercase alphanumeric + hyphens)
|
||||
const sanitized = userId.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||||
return `agent-${sanitized}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate service name (same as deployment)
|
||||
*/
|
||||
static getServiceName(userId: string): string {
|
||||
return this.getDeploymentName(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate PVC name
|
||||
*/
|
||||
static getPvcName(userId: string): string {
|
||||
return `${this.getDeploymentName(userId)}-data`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute MCP endpoint URL from service name
|
||||
*/
|
||||
static getMcpEndpoint(userId: string, namespace: string): string {
|
||||
const serviceName = this.getServiceName(userId);
|
||||
return `http://${serviceName}.${namespace}.svc.cluster.local:3000`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if deployment exists
|
||||
*/
|
||||
async deploymentExists(deploymentName: string): Promise<boolean> {
|
||||
try {
|
||||
await this.appsApi.readNamespacedDeployment(deploymentName, this.config.namespace);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 404) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create agent deployment from template
|
||||
*/
|
||||
async createAgentDeployment(spec: DeploymentSpec): Promise<void> {
|
||||
const deploymentName = KubernetesClient.getDeploymentName(spec.userId);
|
||||
const serviceName = KubernetesClient.getServiceName(spec.userId);
|
||||
const pvcName = KubernetesClient.getPvcName(spec.userId);
|
||||
|
||||
this.config.logger.info(
|
||||
{ userId: spec.userId, licenseType: spec.licenseType, deploymentName },
|
||||
'Creating agent deployment'
|
||||
);
|
||||
|
||||
// Load template based on license type
|
||||
const templatePath = path.join(
|
||||
__dirname,
|
||||
'templates',
|
||||
`${spec.licenseType}-tier.yaml`
|
||||
);
|
||||
|
||||
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
||||
|
||||
// Substitute variables
|
||||
const rendered = templateContent
|
||||
.replace(/\{\{userId\}\}/g, spec.userId)
|
||||
.replace(/\{\{deploymentName\}\}/g, deploymentName)
|
||||
.replace(/\{\{serviceName\}\}/g, serviceName)
|
||||
.replace(/\{\{pvcName\}\}/g, pvcName)
|
||||
.replace(/\{\{agentImage\}\}/g, spec.agentImage)
|
||||
.replace(/\{\{sidecarImage\}\}/g, spec.sidecarImage)
|
||||
.replace(/\{\{storageClass\}\}/g, spec.storageClass);
|
||||
|
||||
// Parse YAML documents (deployment, pvc, service)
|
||||
const documents = yaml.loadAll(rendered) as any[];
|
||||
|
||||
// Apply each resource
|
||||
for (const doc of documents) {
|
||||
if (!doc || !doc.kind) continue;
|
||||
|
||||
try {
|
||||
switch (doc.kind) {
|
||||
case 'Deployment':
|
||||
await this.appsApi.createNamespacedDeployment(this.config.namespace, doc);
|
||||
this.config.logger.info({ deploymentName }, 'Created deployment');
|
||||
break;
|
||||
|
||||
case 'PersistentVolumeClaim':
|
||||
await this.coreApi.createNamespacedPersistentVolumeClaim(
|
||||
this.config.namespace,
|
||||
doc
|
||||
);
|
||||
this.config.logger.info({ pvcName }, 'Created PVC');
|
||||
break;
|
||||
|
||||
case 'Service':
|
||||
await this.coreApi.createNamespacedService(this.config.namespace, doc);
|
||||
this.config.logger.info({ serviceName }, 'Created service');
|
||||
break;
|
||||
|
||||
default:
|
||||
this.config.logger.warn({ kind: doc.kind }, 'Unknown resource kind in template');
|
||||
}
|
||||
} catch (error: any) {
|
||||
// If resource already exists, log warning but continue
|
||||
if (error.response?.statusCode === 409) {
|
||||
this.config.logger.warn(
|
||||
{ kind: doc.kind, name: doc.metadata?.name },
|
||||
'Resource already exists, skipping'
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.config.logger.info({ deploymentName }, 'Agent deployment created successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for deployment to be ready
|
||||
*/
|
||||
async waitForDeploymentReady(
|
||||
deploymentName: string,
|
||||
timeoutMs: number = 120000
|
||||
): Promise<boolean> {
|
||||
const startTime = Date.now();
|
||||
const pollInterval = 2000; // 2 seconds
|
||||
|
||||
this.config.logger.info(
|
||||
{ deploymentName, timeoutMs },
|
||||
'Waiting for deployment to be ready'
|
||||
);
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
try {
|
||||
const response = await this.appsApi.readNamespacedDeployment(
|
||||
deploymentName,
|
||||
this.config.namespace
|
||||
);
|
||||
|
||||
const deployment = response.body;
|
||||
const status = deployment.status;
|
||||
|
||||
// Check if deployment is ready
|
||||
if (
|
||||
status?.availableReplicas &&
|
||||
status.availableReplicas > 0 &&
|
||||
status.readyReplicas &&
|
||||
status.readyReplicas > 0
|
||||
) {
|
||||
this.config.logger.info({ deploymentName }, 'Deployment is ready');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for failure conditions
|
||||
if (status?.conditions) {
|
||||
const failedCondition = status.conditions.find(
|
||||
(c) => c.type === 'Progressing' && c.status === 'False'
|
||||
);
|
||||
if (failedCondition) {
|
||||
this.config.logger.error(
|
||||
{ deploymentName, reason: failedCondition.reason, message: failedCondition.message },
|
||||
'Deployment failed to progress'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.config.logger.debug(
|
||||
{
|
||||
deploymentName,
|
||||
replicas: status?.replicas,
|
||||
ready: status?.readyReplicas,
|
||||
available: status?.availableReplicas,
|
||||
},
|
||||
'Deployment not ready yet, waiting...'
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 404) {
|
||||
this.config.logger.warn({ deploymentName }, 'Deployment not found');
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
this.config.logger.warn({ deploymentName, timeoutMs }, 'Deployment readiness timeout');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service endpoint URL
|
||||
*/
|
||||
async getServiceEndpoint(serviceName: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await this.coreApi.readNamespacedService(
|
||||
serviceName,
|
||||
this.config.namespace
|
||||
);
|
||||
|
||||
const service = response.body;
|
||||
|
||||
// For ClusterIP services, return internal DNS name
|
||||
if (service.spec?.type === 'ClusterIP') {
|
||||
const port = service.spec.ports?.find((p) => p.name === 'mcp')?.port || 3000;
|
||||
return `http://${serviceName}.${this.config.namespace}.svc.cluster.local:${port}`;
|
||||
}
|
||||
|
||||
// For other service types (NodePort, LoadBalancer), would need different logic
|
||||
this.config.logger.warn(
|
||||
{ serviceName, type: service.spec?.type },
|
||||
'Unexpected service type'
|
||||
);
|
||||
return null;
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 404) {
|
||||
this.config.logger.warn({ serviceName }, 'Service not found');
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete deployment and associated resources
|
||||
* (Used for cleanup/testing - normally handled by lifecycle sidecar)
|
||||
*/
|
||||
async deleteAgentDeployment(userId: string): Promise<void> {
|
||||
const deploymentName = KubernetesClient.getDeploymentName(userId);
|
||||
const serviceName = KubernetesClient.getServiceName(userId);
|
||||
const pvcName = KubernetesClient.getPvcName(userId);
|
||||
|
||||
this.config.logger.info({ userId, deploymentName }, 'Deleting agent deployment');
|
||||
|
||||
// Delete deployment
|
||||
try {
|
||||
await this.appsApi.deleteNamespacedDeployment(deploymentName, this.config.namespace);
|
||||
this.config.logger.info({ deploymentName }, 'Deleted deployment');
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode !== 404) {
|
||||
this.config.logger.warn({ deploymentName, error }, 'Failed to delete deployment');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete service
|
||||
try {
|
||||
await this.coreApi.deleteNamespacedService(serviceName, this.config.namespace);
|
||||
this.config.logger.info({ serviceName }, 'Deleted service');
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode !== 404) {
|
||||
this.config.logger.warn({ serviceName, error }, 'Failed to delete service');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete PVC
|
||||
try {
|
||||
await this.coreApi.deleteNamespacedPersistentVolumeClaim(pvcName, this.config.namespace);
|
||||
this.config.logger.info({ pvcName }, 'Deleted PVC');
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode !== 404) {
|
||||
this.config.logger.warn({ pvcName, error }, 'Failed to delete PVC');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
gateway/src/k8s/container-manager.ts
Normal file
118
gateway/src/k8s/container-manager.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import { KubernetesClient, type DeploymentSpec } from './client.js';
|
||||
import type { UserLicense } from '../types/user.js';
|
||||
|
||||
export interface ContainerManagerConfig {
|
||||
k8sClient: KubernetesClient;
|
||||
agentImage: string;
|
||||
sidecarImage: string;
|
||||
storageClass: string;
|
||||
namespace: string;
|
||||
logger: FastifyBaseLogger;
|
||||
}
|
||||
|
||||
export interface ContainerStatus {
|
||||
exists: boolean;
|
||||
ready: boolean;
|
||||
mcpEndpoint: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container manager orchestrates agent container lifecycle
|
||||
*/
|
||||
export class ContainerManager {
|
||||
private config: ContainerManagerConfig;
|
||||
|
||||
constructor(config: ContainerManagerConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user's container is running and ready
|
||||
* Returns the MCP endpoint URL
|
||||
*/
|
||||
async ensureContainerRunning(
|
||||
userId: string,
|
||||
license: UserLicense
|
||||
): Promise<{ mcpEndpoint: string; wasCreated: boolean }> {
|
||||
const deploymentName = KubernetesClient.getDeploymentName(userId);
|
||||
const mcpEndpoint = KubernetesClient.getMcpEndpoint(userId, this.config.namespace);
|
||||
|
||||
this.config.logger.info(
|
||||
{ userId, licenseType: license.licenseType, deploymentName },
|
||||
'Ensuring container is running'
|
||||
);
|
||||
|
||||
// Check if deployment already exists
|
||||
const exists = await this.config.k8sClient.deploymentExists(deploymentName);
|
||||
|
||||
if (exists) {
|
||||
this.config.logger.info({ userId, deploymentName }, 'Container deployment already exists');
|
||||
|
||||
// Wait for it to be ready (in case it's starting up)
|
||||
const ready = await this.config.k8sClient.waitForDeploymentReady(deploymentName, 30000);
|
||||
|
||||
if (!ready) {
|
||||
this.config.logger.warn(
|
||||
{ userId, deploymentName },
|
||||
'Existing deployment not ready within timeout'
|
||||
);
|
||||
// Continue anyway - might be an image pull or other transient issue
|
||||
}
|
||||
|
||||
return { mcpEndpoint, wasCreated: false };
|
||||
}
|
||||
|
||||
// Create new deployment
|
||||
this.config.logger.info({ userId, licenseType: license.licenseType }, 'Creating new container');
|
||||
|
||||
const spec: DeploymentSpec = {
|
||||
userId,
|
||||
licenseType: license.licenseType,
|
||||
agentImage: this.config.agentImage,
|
||||
sidecarImage: this.config.sidecarImage,
|
||||
storageClass: this.config.storageClass,
|
||||
};
|
||||
|
||||
await this.config.k8sClient.createAgentDeployment(spec);
|
||||
|
||||
// Wait for deployment to be ready
|
||||
const ready = await this.config.k8sClient.waitForDeploymentReady(deploymentName, 120000);
|
||||
|
||||
if (!ready) {
|
||||
throw new Error(
|
||||
`Container deployment failed to become ready within timeout: ${deploymentName}`
|
||||
);
|
||||
}
|
||||
|
||||
this.config.logger.info({ userId, mcpEndpoint }, 'Container is ready');
|
||||
|
||||
return { mcpEndpoint, wasCreated: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check container status without creating it
|
||||
*/
|
||||
async getContainerStatus(userId: string): Promise<ContainerStatus> {
|
||||
const deploymentName = KubernetesClient.getDeploymentName(userId);
|
||||
const mcpEndpoint = KubernetesClient.getMcpEndpoint(userId, this.config.namespace);
|
||||
|
||||
const exists = await this.config.k8sClient.deploymentExists(deploymentName);
|
||||
|
||||
if (!exists) {
|
||||
return { exists: false, ready: false, mcpEndpoint };
|
||||
}
|
||||
|
||||
// Check if ready (with short timeout)
|
||||
const ready = await this.config.k8sClient.waitForDeploymentReady(deploymentName, 5000);
|
||||
|
||||
return { exists: true, ready, mcpEndpoint };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete container (for cleanup/testing)
|
||||
*/
|
||||
async deleteContainer(userId: string): Promise<void> {
|
||||
await this.config.k8sClient.deleteAgentDeployment(userId);
|
||||
}
|
||||
}
|
||||
199
gateway/src/k8s/templates/enterprise-tier.yaml
Normal file
199
gateway/src/k8s/templates/enterprise-tier.yaml
Normal file
@@ -0,0 +1,199 @@
|
||||
# Enterprise tier agent deployment template
|
||||
# Variables: {{userId}}, {{deploymentName}}, {{pvcName}}, {{serviceName}}
|
||||
# Enterprise: No idle shutdown, larger resources
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{deploymentName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
app.kubernetes.io/name: agent
|
||||
app.kubernetes.io/component: user-agent
|
||||
dexorder.io/component: agent
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/deployment: {{deploymentName}}
|
||||
dexorder.io/license-tier: enterprise
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
dexorder.io/component: agent
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/deployment: {{deploymentName}}
|
||||
dexorder.io/license-tier: enterprise
|
||||
spec:
|
||||
serviceAccountName: agent-lifecycle
|
||||
shareProcessNamespace: true
|
||||
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
containers:
|
||||
- name: agent
|
||||
image: {{agentImage}}
|
||||
imagePullPolicy: Always
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "1Gi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "4000m"
|
||||
|
||||
env:
|
||||
- name: USER_ID
|
||||
value: {{userId}}
|
||||
- name: IDLE_TIMEOUT_MINUTES
|
||||
value: "0"
|
||||
- name: IDLE_CHECK_INTERVAL_SECONDS
|
||||
value: "60"
|
||||
- name: ENABLE_IDLE_SHUTDOWN
|
||||
value: "false"
|
||||
- name: MCP_SERVER_PORT
|
||||
value: "3000"
|
||||
- name: ZMQ_CONTROL_PORT
|
||||
value: "5555"
|
||||
|
||||
ports:
|
||||
- name: mcp
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
- name: zmq-control
|
||||
containerPort: 5555
|
||||
protocol: TCP
|
||||
|
||||
volumeMounts:
|
||||
- name: agent-data
|
||||
mountPath: /app/data
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: shared-run
|
||||
mountPath: /var/run/agent
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: mcp
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: mcp
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
|
||||
- name: lifecycle-sidecar
|
||||
image: {{sidecarImage}}
|
||||
imagePullPolicy: Always
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "10m"
|
||||
limits:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: DEPLOYMENT_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.labels['dexorder.io/deployment']
|
||||
- name: USER_TYPE
|
||||
value: "enterprise"
|
||||
- name: MAIN_CONTAINER_PID
|
||||
value: "1"
|
||||
|
||||
volumeMounts:
|
||||
- name: shared-run
|
||||
mountPath: /var/run/agent
|
||||
readOnly: true
|
||||
|
||||
volumes:
|
||||
- name: agent-data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{pvcName}}
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 512Mi
|
||||
- name: shared-run
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 1Mi
|
||||
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{pvcName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/license-tier: enterprise
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
storageClassName: {{storageClass}}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{serviceName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/license-tier: enterprise
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
ports:
|
||||
- name: mcp
|
||||
port: 3000
|
||||
targetPort: mcp
|
||||
protocol: TCP
|
||||
- name: zmq-control
|
||||
port: 5555
|
||||
targetPort: zmq-control
|
||||
protocol: TCP
|
||||
198
gateway/src/k8s/templates/free-tier.yaml
Normal file
198
gateway/src/k8s/templates/free-tier.yaml
Normal file
@@ -0,0 +1,198 @@
|
||||
# Free tier agent deployment template
|
||||
# Variables: {{userId}}, {{deploymentName}}, {{pvcName}}, {{serviceName}}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{deploymentName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
app.kubernetes.io/name: agent
|
||||
app.kubernetes.io/component: user-agent
|
||||
dexorder.io/component: agent
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/deployment: {{deploymentName}}
|
||||
dexorder.io/license-tier: free
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
dexorder.io/component: agent
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/deployment: {{deploymentName}}
|
||||
dexorder.io/license-tier: free
|
||||
spec:
|
||||
serviceAccountName: agent-lifecycle
|
||||
shareProcessNamespace: true
|
||||
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
containers:
|
||||
- name: agent
|
||||
image: {{agentImage}}
|
||||
imagePullPolicy: Always
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "256Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
|
||||
env:
|
||||
- name: USER_ID
|
||||
value: {{userId}}
|
||||
- name: IDLE_TIMEOUT_MINUTES
|
||||
value: "15"
|
||||
- name: IDLE_CHECK_INTERVAL_SECONDS
|
||||
value: "60"
|
||||
- name: ENABLE_IDLE_SHUTDOWN
|
||||
value: "true"
|
||||
- name: MCP_SERVER_PORT
|
||||
value: "3000"
|
||||
- name: ZMQ_CONTROL_PORT
|
||||
value: "5555"
|
||||
|
||||
ports:
|
||||
- name: mcp
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
- name: zmq-control
|
||||
containerPort: 5555
|
||||
protocol: TCP
|
||||
|
||||
volumeMounts:
|
||||
- name: agent-data
|
||||
mountPath: /app/data
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: shared-run
|
||||
mountPath: /var/run/agent
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: mcp
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: mcp
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
|
||||
- name: lifecycle-sidecar
|
||||
image: {{sidecarImage}}
|
||||
imagePullPolicy: Always
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "10m"
|
||||
limits:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: DEPLOYMENT_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.labels['dexorder.io/deployment']
|
||||
- name: USER_TYPE
|
||||
value: "free"
|
||||
- name: MAIN_CONTAINER_PID
|
||||
value: "1"
|
||||
|
||||
volumeMounts:
|
||||
- name: shared-run
|
||||
mountPath: /var/run/agent
|
||||
readOnly: true
|
||||
|
||||
volumes:
|
||||
- name: agent-data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{pvcName}}
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 128Mi
|
||||
- name: shared-run
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 1Mi
|
||||
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{pvcName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/license-tier: free
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
storageClassName: {{storageClass}}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{serviceName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/license-tier: free
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
ports:
|
||||
- name: mcp
|
||||
port: 3000
|
||||
targetPort: mcp
|
||||
protocol: TCP
|
||||
- name: zmq-control
|
||||
port: 5555
|
||||
targetPort: zmq-control
|
||||
protocol: TCP
|
||||
198
gateway/src/k8s/templates/pro-tier.yaml
Normal file
198
gateway/src/k8s/templates/pro-tier.yaml
Normal file
@@ -0,0 +1,198 @@
|
||||
# Pro tier agent deployment template
|
||||
# Variables: {{userId}}, {{deploymentName}}, {{pvcName}}, {{serviceName}}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{deploymentName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
app.kubernetes.io/name: agent
|
||||
app.kubernetes.io/component: user-agent
|
||||
dexorder.io/component: agent
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/deployment: {{deploymentName}}
|
||||
dexorder.io/license-tier: pro
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
dexorder.io/component: agent
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/deployment: {{deploymentName}}
|
||||
dexorder.io/license-tier: pro
|
||||
spec:
|
||||
serviceAccountName: agent-lifecycle
|
||||
shareProcessNamespace: true
|
||||
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
fsGroup: 1000
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
containers:
|
||||
- name: agent
|
||||
image: {{agentImage}}
|
||||
imagePullPolicy: Always
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "250m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "2000m"
|
||||
|
||||
env:
|
||||
- name: USER_ID
|
||||
value: {{userId}}
|
||||
- name: IDLE_TIMEOUT_MINUTES
|
||||
value: "60"
|
||||
- name: IDLE_CHECK_INTERVAL_SECONDS
|
||||
value: "60"
|
||||
- name: ENABLE_IDLE_SHUTDOWN
|
||||
value: "true"
|
||||
- name: MCP_SERVER_PORT
|
||||
value: "3000"
|
||||
- name: ZMQ_CONTROL_PORT
|
||||
value: "5555"
|
||||
|
||||
ports:
|
||||
- name: mcp
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
- name: zmq-control
|
||||
containerPort: 5555
|
||||
protocol: TCP
|
||||
|
||||
volumeMounts:
|
||||
- name: agent-data
|
||||
mountPath: /app/data
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: shared-run
|
||||
mountPath: /var/run/agent
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: mcp
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: mcp
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
|
||||
- name: lifecycle-sidecar
|
||||
image: {{sidecarImage}}
|
||||
imagePullPolicy: Always
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 1000
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "32Mi"
|
||||
cpu: "10m"
|
||||
limits:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
|
||||
env:
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: DEPLOYMENT_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.labels['dexorder.io/deployment']
|
||||
- name: USER_TYPE
|
||||
value: "pro"
|
||||
- name: MAIN_CONTAINER_PID
|
||||
value: "1"
|
||||
|
||||
volumeMounts:
|
||||
- name: shared-run
|
||||
mountPath: /var/run/agent
|
||||
readOnly: true
|
||||
|
||||
volumes:
|
||||
- name: agent-data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{pvcName}}
|
||||
- name: tmp
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 256Mi
|
||||
- name: shared-run
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 1Mi
|
||||
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{pvcName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/license-tier: pro
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
storageClassName: {{storageClass}}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{serviceName}}
|
||||
namespace: dexorder-agents
|
||||
labels:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
dexorder.io/license-tier: pro
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
dexorder.io/user-id: {{userId}}
|
||||
ports:
|
||||
- name: mcp
|
||||
port: 3000
|
||||
targetPort: mcp
|
||||
protocol: TCP
|
||||
- name: zmq-control
|
||||
port: 5555
|
||||
targetPort: zmq-control
|
||||
protocol: TCP
|
||||
Reference in New Issue
Block a user