diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a535a56516c86ea1e3e463f50971443b30364e68 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Secret values files — never commit real secrets +environments/**/secrets.values.yaml diff --git a/README.md b/README.md index cf497059659897eafec3564c077a782b94c1e14f..20f190551a2ab397ae320543a73ea72d331f03cb 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,10 @@ Intended for local development, integration testing, and demos. **Not for produc | SRM (Service Resource Manager) | `oop` | Manages application artefacts and lifecycle | | Artifact Manager | `oop` | Stores and serves artefacts | | OEG (Open Exposure Gateway) | `oop` | Northbound API entry point for tenants | -| Federation Manager | `federation-manager` | Inter-operator federation workflows | -| Keycloak | `federation-manager` | OAuth2/OIDC authentication for FM | +| Federation Manager | `oop` | Inter-operator federation workflows | +| Keycloak | `oop` | OAuth2/OIDC authentication for FM | +| AI2 MCP | `oop` | MCP server exposing CAMARA-compliant tools | +| AI2 AI Agent | `oop` | Natural language interface backed by Groq | --- @@ -28,6 +30,21 @@ Intended for local development, integration testing, and demos. **Not for produc --- +## Secrets + +Some components require secrets that must not be committed to git. +Create `environments/kind/secrets.values.yaml` (gitignored) before deploying: + +```yaml +ai2: + secrets: + groqApiKey: "" +``` + +If the file is absent, `helm-deploy.sh` will warn and continue — AI2 deploys with an empty key. + +--- + ## Deploy on kind (one command) ```bash @@ -60,6 +77,8 @@ The scripts can also be run individually from any directory. | Keycloak | http://localhost:30081 | | Keycloak Admin | http://localhost:30081/admin — `admin / admin` | | Federation Manager | http://localhost:30989 | +| AI2 MCP | http://localhost:32004 | +| AI2 AI Agent | http://localhost:32013 | --- @@ -95,8 +114,8 @@ kind delete cluster --name oop-cluster | File | Purpose | |---|---| | `oop-platform-chart/values.yaml` | Base defaults for all components | -| `environments/kind/values.yaml` | kind overrides for `oop-platform` (NodePorts, hostPath, storageClass) | -| `environments/kind/values.fm.yaml` | kind overrides for `federation-manager` subchart | +| `environments/kind/values.yaml` | kind overrides (NodePorts, hostPath, storageClass, pullPolicy) | +| `environments/kind/secrets.values.yaml` | **Gitignored.** Per-environment secrets (see [Secrets](#secrets)) | | `environments/kind/cluster.yaml` | kind cluster definition (port mappings, host mounts) | --- diff --git a/environments/kind/cluster.yaml b/environments/kind/cluster.yaml index 7677709458da21a05312b72d9a525d1702ac33f7..82672884e4788a0d341bd9acf05cdbef6bb5442b 100644 --- a/environments/kind/cluster.yaml +++ b/environments/kind/cluster.yaml @@ -18,13 +18,21 @@ nodes: hostPort: 32263 protocol: TCP - # Federation Services (federation-manager namespace) + # Federation Services (oop namespace) - containerPort: 30081 # Keycloak hostPort: 30081 protocol: TCP - containerPort: 30989 # Federation Manager hostPort: 30989 protocol: TCP + + # AI2 Services (oop namespace) + - containerPort: 32004 # AI2 MCP + hostPort: 32004 + protocol: TCP + - containerPort: 32013 # AI2 AI Agent + hostPort: 32013 + protocol: TCP # Storage volumes for MongoDB persistence extraMounts: diff --git a/environments/kind/values.yaml b/environments/kind/values.yaml index fbaea644ee0a801263d60ebb73ebf0a951fa9840..b98a560e75a18e90198b8b24425d3289cd2ec43c 100644 --- a/environments/kind/values.yaml +++ b/environments/kind/values.yaml @@ -24,8 +24,8 @@ srm: image: pullPolicy: IfNotPresent env: - networkAdapterName: open5gs - networkAdapterBaseUrl: http://open5gs-webui:3000 + networkAdapterName: "oai" + networkAdapterBaseUrl: "" scsAsId: "" artifactManager: service: @@ -79,3 +79,20 @@ federationManager: nodePort: 30081 image: pullPolicy: IfNotPresent + +ai2: + images: + mcp: + pullPolicy: IfNotPresent + aiAgent: + pullPolicy: IfNotPresent + service: + mcp: + type: NodePort + nodePort: 32004 + aiAgent: + type: NodePort + nodePort: 32013 + secrets: + # Required for AI2 to function. Override via environments/kind/secrets.values.yaml — do not set here. + groqApiKey: "" \ No newline at end of file diff --git a/oop-platform-chart/Chart.yaml b/oop-platform-chart/Chart.yaml index 63c58dc16a5def3a471c89096e127125ae009f7c..db8570f4d47187386e8de1712951975deda21e5d 100644 --- a/oop-platform-chart/Chart.yaml +++ b/oop-platform-chart/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: oop-platform -description: Open Operator Platform - Complete platform deployment (SRM, OEG, and Federation Manager) +description: Open Operator Platform - Complete platform deployment (SRM, OEG, Federation Manager and AI2) type: application version: 1.0.0 @@ -12,6 +12,9 @@ keywords: - srm - oeg - federation-manager + - ai2 + - mcp + - ai-agent maintainers: - name: Open Operator Platform Team @@ -33,3 +36,6 @@ dependencies: - name: federation-manager version: "1.0.0" condition: federationManager.enabled + - name: ai2 + version: "1.0.0" + condition: ai2.enabled \ No newline at end of file diff --git a/oop-platform-chart/charts/ai2/Chart.yaml b/oop-platform-chart/charts/ai2/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2ec7b05ce32a0889f3a522a1025e30263a32c12c --- /dev/null +++ b/oop-platform-chart/charts/ai2/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: ai2 +type: application +version: 1.0.0 +appVersion: "1.0.0" +keywords: + - ai2 + - oop + - etsi + - mcp + - ai-agent +home: https://labs.etsi.org/rep/oop/code/ai2 +maintainers: + - name: OOP Team diff --git a/oop-platform-chart/charts/ai2/templates/_helpers.tpl b/oop-platform-chart/charts/ai2/templates/_helpers.tpl new file mode 100644 index 0000000000000000000000000000000000000000..964474e5afa2489f33a71c58fe3fec0534e7aa4e --- /dev/null +++ b/oop-platform-chart/charts/ai2/templates/_helpers.tpl @@ -0,0 +1,50 @@ +{{- define "ai2.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "ai2.fullname" -}} +{{- if .Values.fullnameOverride }} + {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} + {{- $name := default .Chart.Name .Values.nameOverride }} + {{- if contains $name .Release.Name }} + {{- .Release.Name | trunc 63 | trimSuffix "-" }} + {{- else }} + {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} + {{- end }} +{{- end }} +{{- end }} + +{{- define "ai2.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{- define "ai2.labels" -}} +helm.sh/chart: {{ include "ai2.chart" . }} +{{ include "ai2.selectorLabels" . }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +{{- end }} + +{{- define "ai2.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ai2.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "ai2.configMapName" -}} +{{- include "ai2.fullname" . }}-config +{{- end }} + +{{- define "ai2.secretName" -}} +{{- include "ai2.fullname" . }}-secrets +{{- end }} + +{{- define "ai2.mcpServiceName" -}} +{{- include "ai2.fullname" . }}-mcp +{{- end }} + +{{- define "ai2.aiAgentServiceName" -}} +{{- include "ai2.fullname" . }}-ai-agent +{{- end }} diff --git a/oop-platform-chart/charts/ai2/templates/ai-agent-deployment.yaml b/oop-platform-chart/charts/ai2/templates/ai-agent-deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e49338983b8d09fb1b325b7739b0c025ca12eb64 --- /dev/null +++ b/oop-platform-chart/charts/ai2/templates/ai-agent-deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ai2.fullname" . }}-ai-agent + namespace: {{ .Release.Namespace }} + labels: + {{- include "ai2.labels" . | nindent 4 }} + app.kubernetes.io/component: ai-agent +spec: + replicas: {{ .Values.replicaCount.aiAgent }} + selector: + matchLabels: + {{- include "ai2.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: ai-agent + template: + metadata: + labels: + {{- include "ai2.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: ai-agent + spec: + initContainers: + - name: wait-for-mcp + image: curlimages/curl:latest + command: ['sh', '-c'] + args: + - | + until curl -sf http://{{ include "ai2.mcpServiceName" . }}:{{ .Values.config.mcp.port }}/health; do + echo "Waiting for MCP to be ready..." + sleep 3 + done + echo "" + echo "MCP is ready." + containers: + - name: ai-agent + image: "{{ .Values.images.aiAgent.repository }}:{{ .Values.images.aiAgent.tag }}" + imagePullPolicy: {{ .Values.images.aiAgent.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.config.aiAgent.port | int }} + protocol: TCP + env: + - name: APP_PORT + valueFrom: + configMapKeyRef: + name: {{ include "ai2.configMapName" . }} + key: APP_PORT + - name: MCP_SERVER_URL + valueFrom: + configMapKeyRef: + name: {{ include "ai2.configMapName" . }} + key: MCP_SERVER_URL + - name: GROQ_MODEL_NAME + valueFrom: + configMapKeyRef: + name: {{ include "ai2.configMapName" . }} + key: GROQ_MODEL_NAME + - name: GROQ_API_KEY + valueFrom: + secretKeyRef: + name: {{ include "ai2.secretName" . }} + key: GROQ_API_KEY diff --git a/oop-platform-chart/charts/ai2/templates/ai-agent-service.yaml b/oop-platform-chart/charts/ai2/templates/ai-agent-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..045d7e9bb3d6556a7d0b935fceb5e15bdcd64b70 --- /dev/null +++ b/oop-platform-chart/charts/ai2/templates/ai-agent-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ai2.aiAgentServiceName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "ai2.labels" . | nindent 4 }} + app.kubernetes.io/component: ai-agent +spec: + type: {{ .Values.service.aiAgent.type }} + ports: + - name: http + port: {{ .Values.service.aiAgent.port }} + targetPort: http + {{- if eq .Values.service.aiAgent.type "NodePort" }} + nodePort: {{ .Values.service.aiAgent.nodePort }} + {{- end }} + protocol: TCP + selector: + {{- include "ai2.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: ai-agent diff --git a/oop-platform-chart/charts/ai2/templates/configmap.yaml b/oop-platform-chart/charts/ai2/templates/configmap.yaml new file mode 100644 index 0000000000000000000000000000000000000000..25e4983c7ec31814e18532030cb4930ad8200c52 --- /dev/null +++ b/oop-platform-chart/charts/ai2/templates/configmap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "ai2.configMapName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "ai2.labels" . | nindent 4 }} +data: + MCP_PORT: {{ .Values.config.mcp.port | quote }} + OEG_SERVICE_URL: {{ .Values.config.mcp.oegServiceUrl | quote }} + APP_PORT: {{ .Values.config.aiAgent.port | quote }} + GROQ_MODEL_NAME: {{ .Values.config.aiAgent.groqModelName | quote }} + MCP_SERVER_URL: {{ printf "http://%s:%s/mcp" (include "ai2.mcpServiceName" .) .Values.config.mcp.port | quote }} diff --git a/oop-platform-chart/charts/ai2/templates/mcp-deployment.yaml b/oop-platform-chart/charts/ai2/templates/mcp-deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..bb84b034099d0fad94aaba20e41f7e6623b5b496 --- /dev/null +++ b/oop-platform-chart/charts/ai2/templates/mcp-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ai2.fullname" . }}-mcp + namespace: {{ .Release.Namespace }} + labels: + {{- include "ai2.labels" . | nindent 4 }} + app.kubernetes.io/component: mcp +spec: + replicas: {{ .Values.replicaCount.mcp }} + selector: + matchLabels: + {{- include "ai2.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: mcp + template: + metadata: + labels: + {{- include "ai2.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: mcp + spec: + containers: + - name: mcp + image: "{{ .Values.images.mcp.repository }}:{{ .Values.images.mcp.tag }}" + imagePullPolicy: {{ .Values.images.mcp.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.config.mcp.port | int }} + protocol: TCP + env: + - name: MCP_PORT + valueFrom: + configMapKeyRef: + name: {{ include "ai2.configMapName" . }} + key: MCP_PORT + - name: OEG_SERVICE_URL + valueFrom: + configMapKeyRef: + name: {{ include "ai2.configMapName" . }} + key: OEG_SERVICE_URL + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 diff --git a/oop-platform-chart/charts/ai2/templates/mcp-service.yaml b/oop-platform-chart/charts/ai2/templates/mcp-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4e4414fcb3b8f21abc6d2408a6f0956e16b5e656 --- /dev/null +++ b/oop-platform-chart/charts/ai2/templates/mcp-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ai2.mcpServiceName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "ai2.labels" . | nindent 4 }} + app.kubernetes.io/component: mcp +spec: + type: {{ .Values.service.mcp.type }} + ports: + - name: http + port: {{ .Values.service.mcp.port }} + targetPort: http + {{- if eq .Values.service.mcp.type "NodePort" }} + nodePort: {{ .Values.service.mcp.nodePort }} + {{- end }} + protocol: TCP + selector: + {{- include "ai2.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: mcp diff --git a/oop-platform-chart/charts/ai2/templates/secret.yaml b/oop-platform-chart/charts/ai2/templates/secret.yaml new file mode 100644 index 0000000000000000000000000000000000000000..494ba6dc2940ef31c14147ba5f76c191448417f0 --- /dev/null +++ b/oop-platform-chart/charts/ai2/templates/secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "ai2.secretName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "ai2.labels" . | nindent 4 }} +type: Opaque +data: + GROQ_API_KEY: {{ .Values.secrets.groqApiKey | b64enc | quote }} diff --git a/oop-platform-chart/charts/ai2/values.yaml b/oop-platform-chart/charts/ai2/values.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ea537d8ce1bf988f7e9d695158397f3b00bf5071 --- /dev/null +++ b/oop-platform-chart/charts/ai2/values.yaml @@ -0,0 +1,35 @@ +replicaCount: + mcp: 1 + aiAgent: 1 + +images: + mcp: + repository: labs.etsi.org:5050/oop/code/ai2/mcp_module + tag: latest + pullPolicy: IfNotPresent + aiAgent: + repository: labs.etsi.org:5050/oop/code/ai2/ai_agent + tag: latest + pullPolicy: IfNotPresent + +config: + mcp: + port: "8004" + oegServiceUrl: "http://oeg/oeg/1.0.0" + aiAgent: + port: "9013" + groqModelName: "qwen/qwen3-32b" + +secrets: + # Required. Set via environments/kind/secrets.values.yaml (gitignored) — NOT recommended to set here. + groqApiKey: "" + +service: + mcp: + type: NodePort + port: 8004 + nodePort: 32004 + aiAgent: + type: NodePort + port: 9013 + nodePort: 32013 \ No newline at end of file diff --git a/oop-platform-chart/charts/oeg/templates/oegcontroller-deployment.yaml b/oop-platform-chart/charts/oeg/templates/oegcontroller-deployment.yaml index 8278041341c12f7abc02f8715094907e343db96e..70d4c03107fda111bf7834a76218f9b20ad2c0fd 100644 --- a/oop-platform-chart/charts/oeg/templates/oegcontroller-deployment.yaml +++ b/oop-platform-chart/charts/oeg/templates/oegcontroller-deployment.yaml @@ -49,7 +49,7 @@ spec: - name: TOKEN_ENDPOINT value: {{ .Values.oegcontroller.env.tokenEndpoint | quote }} - name: AVAIL_ZONE_NOTIF_LINK - value: {{ default "" .Values.oegcontroller.env.availZoneNotifLink | quote }} + value: {{ .Values.oegcontroller.env.availZoneNotifLink | quote }} {{- with .Values.oegcontroller.resources }} resources: {{- toYaml . | nindent 12 }} diff --git a/oop-platform-chart/values.yaml b/oop-platform-chart/values.yaml index b8b508c05260bf08bb47de69dc0005be8f7fa689..03af9155ee8f75719744eb39bcb8da402812b303 100644 --- a/oop-platform-chart/values.yaml +++ b/oop-platform-chart/values.yaml @@ -48,9 +48,9 @@ srm: name: srmcontroller replicaCount: 1 image: - repository: labs.etsi.org:5050/oop/code/service-resource-manager + repository: labs.etsi.org:5050/oop/code/service-resource-manager tag: 1.0.0 - pullPolicy: Always + pullPolicy: IfNotPresent service: name: srm type: ClusterIP @@ -130,9 +130,9 @@ oeg: name: oegcontroller replicaCount: 1 image: - repository: labs.etsi.org:5050/oop/code/open-exposure-gateway + repository: labs.etsi.org:5050/oop/code/open-exposure-gateway tag: 1.0.0 - pullPolicy: Always + pullPolicy: IfNotPresent service: name: oeg type: ClusterIP @@ -152,7 +152,6 @@ federationManager: global: namespace: oop - # --- MongoDB FOR FM (THIS WAS MISSING!) mongodb: enabled: true name: mongodb @@ -201,3 +200,47 @@ federationManager: openvpn: enabled: false + +# ==================================================================== +# AI2 (MCP + AI Agent) +# ==================================================================== +ai2: + enabled: true + fullnameOverride: "ai2" + + replicaCount: + mcp: 1 + aiAgent: 1 + + images: + mcp: + repository: labs.etsi.org:5050/oop/code/ai2/mcp_module + tag: latest + pullPolicy: IfNotPresent + aiAgent: + repository: labs.etsi.org:5050/oop/code/ai2/ai_agent + tag: latest + pullPolicy: IfNotPresent + + config: + mcp: + port: "8004" + # Derived from oeg.oegcontroller.service.name + oeg.oegcontroller.service.port + oegServiceUrl: "http://oeg/oeg/1.0.0" + aiAgent: + port: "9013" + groqModelName: "qwen/qwen3-32b" + + secrets: + # Required. Set via environments/kind/secrets.values.yaml (gitignored) — NOT recommended to set here. + groqApiKey: "" + + service: + mcp: + type: ClusterIP + port: 8004 + nodePort: "" + aiAgent: + type: ClusterIP + port: 9013 + nodePort: "" diff --git a/scripts/helm-deploy.sh b/scripts/helm-deploy.sh index 2d43f5d0a0a7fc741172df80aa363ba674041492..92cad6050622e2f722bb3b26a69a2797e5b99b27 100755 --- a/scripts/helm-deploy.sh +++ b/scripts/helm-deploy.sh @@ -29,9 +29,24 @@ if ! command -v helm &> /dev/null; then exit 1 fi +# ── Load secrets file ──────────────────────────────────────────────── +SECRETS_FILE="$REPO_ROOT/environments/kind/secrets.values.yaml" + +if [[ -f "$SECRETS_FILE" ]]; then + echo "Secrets file found: environments/kind/secrets.values.yaml" + SECRETS_FLAGS="-f $SECRETS_FILE" +else + echo "WARNING: environments/kind/secrets.values.yaml not found." + echo " See README.md for the required format." + echo " AI2 will deploy but the Groq API key will be empty." + echo "" + SECRETS_FLAGS="" +fi +echo "" + # ── Generate token from existing service account ───────────────────── echo "Generating token..." -TOKEN=$(kubectl -n $OOP_NS create token oop-user) +TOKEN=$(kubectl -n $OOP_NS create token oop-user --duration=720h) echo " Token generated" echo "" @@ -42,6 +57,7 @@ helm upgrade --install $OOP_RELEASE "$CHART_DIR" \ -n $OOP_NS \ --create-namespace \ -f "$REPO_ROOT/environments/kind/values.yaml" \ + ${SECRETS_FLAGS} \ --set srm.srmcontroller.env.kubernetesMasterToken="$TOKEN" \ --set federationManager.enabled=true @@ -57,6 +73,8 @@ echo " OEG API: http://localhost:32263/oeg/1.0.0/docs/" echo " Keycloak: http://localhost:30081" echo " Keycloak Admin: http://localhost:30081/admin (Username: admin / Password: admin)" echo " Federation Manager: http://localhost:30989" +echo " AI2 MCP: http://localhost:32004" +echo " AI2 AI Agent: http://localhost:32013" echo "" echo "Useful commands:" echo " kubectl get pods -n $OOP_NS"