diff --git a/Dockerfile b/Dockerfile index 80807756..9617e6b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,6 +75,4 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ # Replace ``your_app:server.app`` with the import path of your own # AgentServer instance. The placeholder example below assumes a module # called ``app.py`` at /home/locus/app.py exposing a ``server`` symbol. -# -# An example ``app.py`` lives in deploy/oci-functions/app.py. CMD ["uvicorn", "app:server.app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/README.md b/README.md index 60ae2d70..13f9804b 100644 --- a/README.md +++ b/README.md @@ -304,10 +304,11 @@ server = AgentServer(agent=my_agent, api_key=os.environ["API_KEY"]) server.run(host="0.0.0.0", port=8080) ``` -The repo ships a multi-stage `Dockerfile` and a Helm chart at -[`deploy/helm/locus-agent/`](deploy/helm/locus-agent/) — Deployment, HPA, Ingress, OCI workload-identity hooks. +The repo ships a multi-stage `Dockerfile` ready to drop into your own image +pipeline. Deploy anywhere FastAPI runs — OCI Functions, Container Instances, +OKE, Compute, or any cloud equivalent. -→ [Deploy guide](https://oracle-samples.github.io/locus/tutorials/deploy/) +→ [Deploy guide](https://oracle-samples.github.io/locus/how-to/deploy/) --- diff --git a/deploy/helm/locus-agent/.helmignore b/deploy/helm/locus-agent/.helmignore deleted file mode 100644 index b6e17c9b..00000000 --- a/deploy/helm/locus-agent/.helmignore +++ /dev/null @@ -1,11 +0,0 @@ -# Patterns to ignore when building Helm packages. -.DS_Store -.git/ -.gitignore -.idea/ -.vscode/ -*.tmproj -*.swp -*.bak -*.tgz -README.md.bak diff --git a/deploy/helm/locus-agent/Chart.yaml b/deploy/helm/locus-agent/Chart.yaml deleted file mode 100644 index 0ac4ae54..00000000 --- a/deploy/helm/locus-agent/Chart.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v2 -name: locus-agent -description: | - A locus AgentServer deployment. Wraps any locus.Agent in a FastAPI app - with /invoke, /stream (SSE), /threads/{id}, and /health endpoints, plus - per-principal thread isolation when api_key is configured. -type: application -version: 0.1.0 -appVersion: 0.1.0 -home: https://github.com/oracle-samples/locus -sources: -- https://github.com/oracle-samples/locus -keywords: -- oci -- agent -- llm -- generative-ai -maintainers: -- name: locus contributors - url: https://github.com/oracle-samples/locus diff --git a/deploy/helm/locus-agent/README.md b/deploy/helm/locus-agent/README.md deleted file mode 100644 index 349a53f1..00000000 --- a/deploy/helm/locus-agent/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# locus-agent Helm chart - -Deploys a [locus](https://github.com/oracle-samples/locus) `AgentServer` -on Kubernetes / OKE. Wraps any `locus.Agent` in a FastAPI app exposing -`/invoke`, `/stream` (SSE), `/threads/{id}`, and `/health`. - -## Quick start - -```bash -helm install locus-agent ./deploy/helm/locus-agent \ - --set image.repository=ghcr.io/your-org/locus-agent \ - --set image.tag=v0.1.0 \ - --set auth.apiKey=$(openssl rand -hex 16) \ - --set ociBucket.enabled=true \ - --set ociBucket.bucketName=locus-threads \ - --set ociBucket.namespace= -``` - -## Values - -See `values.yaml` for the full set. Notable knobs: - -| Key | Default | Purpose | -|---|---|---| -| `replicaCount` | `2` | Replicas (scale via HPA when enabled). | -| `auth.apiKey` | `""` | Bearer-token API key. Use `auth.existingSecret` instead in prod. | -| `serviceAccount.annotations` | `{}` | Add OCI workload-identity annotations here. | -| `probes.liveness.path` | `/health` | Liveness endpoint. | -| `ociBucket.enabled` | `false` | Wire OCI Object Storage as the checkpointer backend. | -| `autoscaling.enabled` | `false` | Render an HPA. | -| `ingress.enabled` | `false` | Render an Ingress. | - -## Auth - -The chart expects a bearer-token secret named in -`auth.existingSecret` or auto-created from `auth.apiKey`. The -container reads it from `LOCUS_SERVER_API_KEY` and passes it to -`AgentServer(api_key=...)`. Per-principal thread namespacing is -enforced server-side — two API keys can't read each other's threads. - -## OCI workload identity - -Preferred over static `apiKey`: enable workload identity on the OKE -node pool, then add the IAM role annotation to the chart's service -account: - -```yaml -serviceAccount: - annotations: - workload.identity.oci.oraclecloud.com/role: arn:oci:... -``` - -The `OCIBucketBackend` will pick up `instance_principal` / -`resource_principal` automatically — no static credentials needed. - -## What you still own - -- The `app.py` module the container runs (see the `Dockerfile`'s `CMD`). - This is where you instantiate your `Agent` + `AgentServer`. -- The image build + registry push. -- Network policy, monitoring (Prometheus scrape configs, Grafana - dashboards), and observability (OTLP exporter env vars). - -## See also - -- [`docs/how-to/deploy.md`](../../../docs/how-to/deploy.md) — full - deployment walkthrough. -- [`docs/concepts/server.md`](../../../docs/concepts/server.md) — - the `AgentServer` API. diff --git a/deploy/helm/locus-agent/templates/_helpers.tpl b/deploy/helm/locus-agent/templates/_helpers.tpl deleted file mode 100644 index 7fa230cd..00000000 --- a/deploy/helm/locus-agent/templates/_helpers.tpl +++ /dev/null @@ -1,66 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "locus-agent.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Fully-qualified app name. -*/}} -{{- define "locus-agent.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 -}} - -{{/* -Common labels. -*/}} -{{- define "locus-agent.labels" -}} -helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }} -app.kubernetes.io/name: {{ include "locus-agent.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end -}} - -{{/* -Selector labels. -*/}} -{{- define "locus-agent.selectorLabels" -}} -app.kubernetes.io/name: {{ include "locus-agent.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end -}} - -{{/* -Service account name. Returns the explicit name or auto-derives. -*/}} -{{- define "locus-agent.serviceAccountName" -}} -{{- if .Values.serviceAccount.create -}} -{{- default (include "locus-agent.fullname" .) .Values.serviceAccount.name -}} -{{- else -}} -{{- default "default" .Values.serviceAccount.name -}} -{{- end -}} -{{- end -}} - -{{/* -Bearer-token secret name. References an existing secret if provided, -otherwise the chart-managed one. -*/}} -{{- define "locus-agent.authSecretName" -}} -{{- if .Values.auth.existingSecret -}} -{{- .Values.auth.existingSecret -}} -{{- else -}} -{{- printf "%s-auth" (include "locus-agent.fullname" .) -}} -{{- end -}} -{{- end -}} diff --git a/deploy/helm/locus-agent/templates/deployment.yaml b/deploy/helm/locus-agent/templates/deployment.yaml deleted file mode 100644 index 18226aee..00000000 --- a/deploy/helm/locus-agent/templates/deployment.yaml +++ /dev/null @@ -1,82 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "locus-agent.fullname" . }} - labels: - {{- include "locus-agent.labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include "locus-agent.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - {{- include "locus-agent.selectorLabels" . | nindent 8 }} - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - serviceAccountName: {{ include "locus-agent.serviceAccountName" . }} - containers: - - name: locus-agent - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: 8080 - protocol: TCP - env: - - name: LOCUS_SERVER_API_KEY - valueFrom: - secretKeyRef: - name: {{ include "locus-agent.authSecretName" . }} - key: {{ .Values.auth.secretKey }} - {{- if .Values.ociBucket.enabled }} - - name: LOCUS_OCI_BUCKET_NAME - value: {{ .Values.ociBucket.bucketName | quote }} - - name: LOCUS_OCI_NAMESPACE - value: {{ .Values.ociBucket.namespace | quote }} - - name: LOCUS_OCI_BUCKET_PREFIX - value: {{ .Values.ociBucket.prefix | quote }} - {{- end }} - {{- with .Values.env }} - {{- toYaml . | nindent 12 }} - {{- end }} - livenessProbe: - httpGet: - path: {{ .Values.probes.liveness.path }} - port: http - initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} - periodSeconds: {{ .Values.probes.liveness.periodSeconds }} - timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }} - readinessProbe: - httpGet: - path: {{ .Values.probes.readiness.path }} - port: http - initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} - periodSeconds: {{ .Values.probes.readiness.periodSeconds }} - timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }} - startupProbe: - httpGet: - path: {{ .Values.probes.startup.path }} - port: http - failureThreshold: {{ .Values.probes.startup.failureThreshold }} - periodSeconds: {{ .Values.probes.startup.periodSeconds }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/deploy/helm/locus-agent/templates/hpa.yaml b/deploy/helm/locus-agent/templates/hpa.yaml deleted file mode 100644 index 343dce4e..00000000 --- a/deploy/helm/locus-agent/templates/hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.autoscaling.enabled -}} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "locus-agent.fullname" . }} - labels: - {{- include "locus-agent.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "locus-agent.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} diff --git a/deploy/helm/locus-agent/templates/ingress.yaml b/deploy/helm/locus-agent/templates/ingress.yaml deleted file mode 100644 index 4b741fc8..00000000 --- a/deploy/helm/locus-agent/templates/ingress.yaml +++ /dev/null @@ -1,36 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "locus-agent.fullname" . -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "locus-agent.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if .Values.ingress.className }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- toYaml .Values.ingress.tls | nindent 4 }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - pathType: {{ .pathType }} - backend: - service: - name: {{ $fullName }} - port: - name: http - {{- end }} - {{- end }} -{{- end }} diff --git a/deploy/helm/locus-agent/templates/secret.yaml b/deploy/helm/locus-agent/templates/secret.yaml deleted file mode 100644 index e38fba49..00000000 --- a/deploy/helm/locus-agent/templates/secret.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{- if and (not .Values.auth.existingSecret) .Values.auth.apiKey -}} -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "locus-agent.authSecretName" . }} - labels: - {{- include "locus-agent.labels" . | nindent 4 }} -type: Opaque -stringData: - {{ .Values.auth.secretKey }}: {{ .Values.auth.apiKey | quote }} -{{- end }} diff --git a/deploy/helm/locus-agent/templates/service.yaml b/deploy/helm/locus-agent/templates/service.yaml deleted file mode 100644 index f0ae5690..00000000 --- a/deploy/helm/locus-agent/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "locus-agent.fullname" . }} - labels: - {{- include "locus-agent.labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - name: http - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - selector: - {{- include "locus-agent.selectorLabels" . | nindent 4 }} diff --git a/deploy/helm/locus-agent/templates/serviceaccount.yaml b/deploy/helm/locus-agent/templates/serviceaccount.yaml deleted file mode 100644 index 2d2e36e1..00000000 --- a/deploy/helm/locus-agent/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "locus-agent.serviceAccountName" . }} - labels: - {{- include "locus-agent.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/deploy/helm/locus-agent/values.yaml b/deploy/helm/locus-agent/values.yaml deleted file mode 100644 index cfa810ec..00000000 --- a/deploy/helm/locus-agent/values.yaml +++ /dev/null @@ -1,105 +0,0 @@ -# Default values for locus-agent. -# Override with `helm install -f overrides.yaml`. - -image: - repository: ghcr.io/oracle-samples/locus-agent - tag: latest - pullPolicy: IfNotPresent - -# Number of replicas. Bump for HA + horizontal scale; ensure the -# checkpointer (e.g., OCIBucketBackend) is shared so all replicas see -# the same threads. -replicaCount: 2 - -# AgentServer auth — set the bearer token via a Kubernetes Secret. -# When `existingSecret` is set, the chart references that secret name -# and `secretKey`; otherwise it creates one with `apiKey` literal. -auth: - existingSecret: '' # e.g. "locus-agent-secrets" - secretKey: LOCUS_SERVER_API_KEY - apiKey: '' # only used if existingSecret is empty - -# Workload identity — preferred OCI auth path on OKE. The agent picks -# up Instance / Resource Principals from the underlying VM/pod. -serviceAccount: - create: true - name: '' # auto-derived if empty - annotations: {} # add OCI workload identity annotations here - -# Container resource requests / limits. -resources: - requests: - cpu: 250m - memory: 512Mi - limits: - cpu: 1000m - memory: 1Gi - -# Liveness + readiness probes hit /health. Tighten startup to allow for -# cold-import time on first boot. -probes: - liveness: - path: /health - initialDelaySeconds: 15 - periodSeconds: 20 - timeoutSeconds: 5 - readiness: - path: /health - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 3 - startup: - path: /health - failureThreshold: 30 - periodSeconds: 5 - -# Service exposure — ClusterIP by default; flip to LoadBalancer or -# NodePort for direct external access. -service: - type: ClusterIP - port: 8080 - -# Optional ingress. Disabled by default; set `enabled: true` to render -# an Ingress with the configured hosts. -ingress: - enabled: false - className: '' - annotations: {} - hosts: - - host: locus-agent.example.com - paths: - - path: / - pathType: Prefix - tls: [] - -# Horizontal Pod Autoscaler — disabled by default. Enable for -# production workloads with variable traffic. -autoscaling: - enabled: false - minReplicas: 2 - maxReplicas: 10 - targetCPUUtilizationPercentage: 70 - targetMemoryUtilizationPercentage: 80 - -# OCI bucket checkpointer config — set when the agent uses -# OCIBucketBackend for thread persistence. -ociBucket: - enabled: false - bucketName: '' - namespace: '' - prefix: locus/threads/ - -# Free-form environment variables passed to the container. -env: [] - # - name: OCI_PROFILE - # value: DEFAULT - # - name: OTEL_EXPORTER_OTLP_ENDPOINT - # value: http://otel-collector.observability.svc:4318 - -# Pod-level annotations (e.g., for Prometheus scraping or OCI tagging). -podAnnotations: {} - -# Node selection / tolerations / affinity. -nodeSelector: {} -tolerations: [] -affinity: {} diff --git a/deploy/locus-workbench/Makefile b/deploy/locus-workbench/Makefile deleted file mode 100644 index 2758f946..00000000 --- a/deploy/locus-workbench/Makefile +++ /dev/null @@ -1,159 +0,0 @@ -# locus-workbench — full free-tier deploy on OCI Always-Free OKE. -# -# One-shot from a clean tenancy: -# 1. cp cimientos/terraform/terraform.tfvars.example cimientos/terraform/terraform.tfvars -# then fill in tenancy_ocid + compartment_ocid + node_pool_image_id -# 2. make tf-apply (provisions VCN + OKE + OCIR repo) -# 3. make kubeconfig (writes ~/.kube/locus-workbench.kubeconfig) -# 4. make ocir-login (paste an auth token from the OCI Console) -# 5. make docker-push (builds + pushes the workbench image) -# 6. make ocir-secret (creates the pull secret in the cluster) -# 7. make helm-install (deploys the chart) -# 8. make url (prints the public IP — open in a browser) -# -# Tear down with `make destroy` when done. - -SHELL := /usr/bin/env bash - -# === Where things live ======================================================= -REPO_ROOT := $(abspath ../..) -TF_DIR := cimientos/terraform -HELM_DIR := helm/locus-workbench -KUBECONFIG_PATH := $(HOME)/.kube/locus-workbench.kubeconfig -RELEASE_NAME := locus-workbench -NAMESPACE := default - -# === Helpers — let make discover terraform outputs lazily ==================== -TF_OUTPUT := cd $(TF_DIR) && terraform output -raw -CLUSTER_ID = $(shell $(TF_OUTPUT) cluster_id 2>/dev/null) -OCIR_REF = $(shell $(TF_OUTPUT) ocir_image_ref 2>/dev/null) -OCIR_NS = $(shell $(TF_OUTPUT) ocir_namespace 2>/dev/null) -KUBE_CMD = $(shell $(TF_OUTPUT) kubeconfig_command 2>/dev/null) -KUBECTL := kubectl --kubeconfig $(KUBECONFIG_PATH) --namespace $(NAMESPACE) -IMAGE_TAG ?= 0.2.0b9 - -# === Terraform =============================================================== - -.PHONY: tf-init -tf-init: - cd $(TF_DIR) && terraform init - -.PHONY: tf-plan -tf-plan: tf-init - cd $(TF_DIR) && terraform plan -out=tfplan - -.PHONY: tf-apply -tf-apply: tf-init - cd $(TF_DIR) && terraform apply -auto-approve - -.PHONY: tf-output -tf-output: - cd $(TF_DIR) && terraform output - -.PHONY: destroy -destroy: - @echo "WARNING: this destroys the OKE cluster, VCN, and OCIR repo." - @read -p "Type 'yes' to continue: " confirm && [ "$$confirm" = "yes" ] - -helm uninstall $(RELEASE_NAME) --kubeconfig $(KUBECONFIG_PATH) --namespace $(NAMESPACE) || true - cd $(TF_DIR) && terraform destroy -auto-approve - -# === kubeconfig ============================================================== - -.PHONY: kubeconfig -kubeconfig: - mkdir -p $(HOME)/.kube - @echo "Running: $(KUBE_CMD)" - @$(KUBE_CMD) - @echo "Wrote $(KUBECONFIG_PATH)" - @$(KUBECTL) get nodes - -# === OCIR / Docker =========================================================== - -# OCIR auth uses your OCI username + an auth token (NOT the password). -# Generate the token at OCI Console → User → Auth Tokens. -.PHONY: ocir-login -ocir-login: - @echo "OCIR namespace: $(OCIR_NS)" - @echo "When prompted, username is '/'." - @echo "Password is an Auth Token from User → Auth Tokens (NOT your console password)." - docker login $$(echo $(OCIR_REF) | cut -d/ -f1) - -.PHONY: docker-build -docker-build: - docker build \ - -t $(OCIR_REF):$(IMAGE_TAG) \ - -t $(OCIR_REF):latest \ - -f $(REPO_ROOT)/workbench/Dockerfile \ - $(REPO_ROOT) - -.PHONY: docker-push -docker-push: docker-build - docker push $(OCIR_REF):$(IMAGE_TAG) - docker push $(OCIR_REF):latest - -# === Cluster secrets ========================================================= - -# Creates the pull secret the Deployment references. Reuses the -# docker login credentials in ~/.docker/config.json. -.PHONY: ocir-secret -ocir-secret: - $(KUBECTL) delete secret ocir-pull-secret --ignore-not-found - $(KUBECTL) create secret generic ocir-pull-secret \ - --from-file=.dockerconfigjson=$(HOME)/.docker/config.json \ - --type=kubernetes.io/dockerconfigjson - -# Optional: bake API keys into a secret the Helm chart can mount. -# Override OPENAI_API_KEY / ANTHROPIC_API_KEY in your shell first. -.PHONY: api-keys-secret -api-keys-secret: - $(KUBECTL) delete secret locus-workbench-keys --ignore-not-found - $(KUBECTL) create secret generic locus-workbench-keys \ - $(if $(OPENAI_API_KEY),--from-literal=OPENAI_API_KEY=$(OPENAI_API_KEY)) \ - $(if $(ANTHROPIC_API_KEY),--from-literal=ANTHROPIC_API_KEY=$(ANTHROPIC_API_KEY)) - -# === Helm ==================================================================== - -.PHONY: helm-install -helm-install: - helm upgrade --install $(RELEASE_NAME) $(HELM_DIR) \ - --kubeconfig $(KUBECONFIG_PATH) \ - --namespace $(NAMESPACE) \ - --set image.repository=$(OCIR_REF) \ - --set image.tag=$(IMAGE_TAG) \ - --wait --timeout=10m - -.PHONY: helm-uninstall -helm-uninstall: - helm uninstall $(RELEASE_NAME) \ - --kubeconfig $(KUBECONFIG_PATH) --namespace $(NAMESPACE) - -# === Inspection ============================================================== - -.PHONY: status -status: - @echo "--- Nodes ---" - @$(KUBECTL) get nodes - @echo "--- Pods ---" - @$(KUBECTL) get pods -l app.kubernetes.io/name=locus-workbench - @echo "--- Service ---" - @$(KUBECTL) get svc -l app.kubernetes.io/name=locus-workbench - -.PHONY: logs -logs: - $(KUBECTL) logs -l app.kubernetes.io/name=locus-workbench --tail=200 -f - -.PHONY: url -url: - @ip=$$($(KUBECTL) get svc $(RELEASE_NAME) -o jsonpath='{.status.loadBalancer.ingress[0].ip}'); \ - if [ -z "$$ip" ]; then \ - echo "LoadBalancer not ready yet — re-run in a minute."; \ - $(KUBECTL) describe svc $(RELEASE_NAME) | tail -20; \ - else \ - echo "http://$$ip"; \ - fi - -# === One-shot deploy (after Terraform + Docker login) ======================== - -.PHONY: deploy -deploy: kubeconfig docker-push ocir-secret helm-install - @$(MAKE) --no-print-directory url diff --git a/deploy/locus-workbench/README.md b/deploy/locus-workbench/README.md deleted file mode 100644 index 47378533..00000000 --- a/deploy/locus-workbench/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# `locus-workbench` — Always-Free OKE deployment - -Terraform stack + Helm chart that puts the locus workbench on a -managed Kubernetes pod inside your OCI Always-Free tenancy. Single -ARM A1.Flex worker, free Flex 10 Mbps load balancer, free OCIR -repository. Nothing in this stack bills outside the Always-Free -envelope. - -## Layout - -| Path | What it does | -|---|---| -| `cimientos/terraform/` | VCN + IGW + 2 subnets + NSGs + BASIC OKE cluster + ARM node pool + OCIR repo | -| `helm/locus-workbench/` | Single-pod Deployment running all three workbench tiers, plus LoadBalancer Service | -| `Makefile` | One-shot targets — `tf-apply`, `kubeconfig`, `docker-push`, `helm-install`, `url`, `destroy` | - -## One-shot deploy - -```bash -cd deploy/locus-workbench - -# 1. Fill in the two required OCIDs (tenancy + compartment) + -# the worker image OCID. -cp cimientos/terraform/terraform.tfvars.example cimientos/terraform/terraform.tfvars -$EDITOR cimientos/terraform/terraform.tfvars - -# 2. Provision the network + cluster + registry (~10 min on first apply). -make tf-apply - -# 3. Wire kubectl to the new cluster. -make kubeconfig - -# 4. Log in to OCIR (paste an Auth Token, not your console password). -make ocir-login - -# 5. Build + push the workbench image + deploy. -make deploy -``` - -`make deploy` runs `docker-push → ocir-secret → helm-install` end -to end. When it finishes, `make url` prints the public URL. - -## What you pay - -| Resource | Free-tier allowance | This stack uses | -|---|---|---| -| OKE BASIC cluster (control plane) | 1 cluster, free | 1 cluster | -| Worker compute (ARM A1.Flex) | 4 OCPU + 24 GB total | 2 OCPU + 12 GB | -| Flex 10 Mbps load balancer | 1, free | 1 | -| OCIR storage | 1 GB | ~600 MB (workbench image) | -| Egress | 10 TB/month | well under | - -Everything bills **$0** as long as you stay on the Always-Free -allocation. The Terraform stack only provisions Always-Free SKUs by -default; override only if you intentionally want to pay. - -## Iterating on the workbench - -```bash -# After changing workbench/ source: -make docker-push # rebuild + push a new image tag -$(MAKE) IMAGE_TAG=0.2.0b10 deploy # roll the cluster onto the new tag -``` - -## Tearing it all down - -```bash -make destroy -``` - -Confirms with a `yes` prompt, then removes the Helm release, OKE -cluster, node pool, OCIR repo, VCN, and all subnets. State file -stays in `cimientos/terraform/terraform.tfstate` until you delete it. - -## Further reading - -- [Deploy how-to with full walkthrough](../../docs/how-to/deploy-workbench-free-tier.md) -- [Workbench guide](../../docs/workbench.md) -- [Reference infrastructure pattern]( team's production) — production-grade equivalent this stack borrows from diff --git a/deploy/locus-workbench/cimientos/terraform/.gitignore b/deploy/locus-workbench/cimientos/terraform/.gitignore deleted file mode 100644 index 1e18125d..00000000 --- a/deploy/locus-workbench/cimientos/terraform/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Terraform working state — do not commit -.terraform/ -.terraform.lock.hcl -*.tfstate -*.tfstate.* -*.tfplan -terraform.tfvars diff --git a/deploy/locus-workbench/cimientos/terraform/backend.tf b/deploy/locus-workbench/cimientos/terraform/backend.tf deleted file mode 100644 index 29598c3e..00000000 --- a/deploy/locus-workbench/cimientos/terraform/backend.tf +++ /dev/null @@ -1,40 +0,0 @@ -# Remote state in OCI Object Storage via the S3-compat endpoint. -# The bucket itself is provisioned out-of-band by -# scripts/bootstrap-bucket.sh (chicken-and-egg — can't store this -# state IN this state). -# -# Required env vars for the S3-compat creds: -# AWS_ACCESS_KEY_ID OCI Customer Secret Key access key -# AWS_SECRET_ACCESS_KEY OCI Customer Secret Key secret -# -# Generate the Customer Secret Key in the OCI Console: -# User → Customer Secret Keys → Generate - -terraform { - backend "s3" { - region = "ca-toronto-1" - bucket = "locus-workbench-tfstate" - key = "terraform.tfstate" - - # The endpoint is namespace-scoped. bootstrap-bucket.sh prints - # the right URL; pass it on init: - # terraform init -backend-config=endpoints='{"s3":""}' - - # OCI S3 compatibility quirks — these flags make Terraform play - # nicely with Oracle's S3-compat API. The names differ between - # terraform 1.5.x (use_path_style) and 1.6+ (use_path_style); - # the 1.5.x names are kept here for compatibility with the - # widely-shipped Homebrew terraform 1.5.7. CI runs 1.9.8 and - # silently accepts either spelling. - use_path_style = true - skip_credentials_validation = true - skip_metadata_api_check = true - skip_region_validation = true - skip_requesting_account_id = true - # OCI S3-compat returns 501 NotImplemented on chunked uploads; - # skipping the per-chunk SHA256 checksum keeps the SDK on the - # single-PUT path. Without this, state writes fail with - # "AWS chunked encoding not supported". - skip_s3_checksum = true - } -} diff --git a/deploy/locus-workbench/cimientos/terraform/locals.tf b/deploy/locus-workbench/cimientos/terraform/locals.tf deleted file mode 100644 index b8ffa505..00000000 --- a/deploy/locus-workbench/cimientos/terraform/locals.tf +++ /dev/null @@ -1,12 +0,0 @@ -locals { - name = var.name_prefix - common_tags = { - "Project" = "locus-workbench" - "ManagedBy" = "terraform" - "FreeTier" = "true" - "DeployedAt" = timestamp() - } - # Strip "ca-toronto-1" → "tor1" etc. for VCN DNS labels (max 15 - # chars, alphanum only). - region_suffix = lower(replace(replace(var.region, "-", ""), "[^a-z0-9]", "")) -} diff --git a/deploy/locus-workbench/cimientos/terraform/network.tf b/deploy/locus-workbench/cimientos/terraform/network.tf deleted file mode 100644 index 28814797..00000000 --- a/deploy/locus-workbench/cimientos/terraform/network.tf +++ /dev/null @@ -1,195 +0,0 @@ -# Minimal VCN for the workbench: one public subnet that hosts both -# the workers (so kubelet can reach the public OKE API endpoint -# without a NAT hop) and the LoadBalancer service. Free-tier -# topology — no NAT GW, no private subnet. Production deployments -# should use a NAT GW with workers in a private subnet for a tighter blast radius. - -resource "oci_core_vcn" "workbench" { - compartment_id = var.compartment_ocid - cidr_blocks = [var.vcn_cidr] - display_name = "${local.name}-vcn" - dns_label = "lwb${local.region_suffix}" - freeform_tags = local.common_tags -} - -resource "oci_core_internet_gateway" "workbench" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.workbench.id - display_name = "${local.name}-igw" - enabled = true - freeform_tags = local.common_tags -} - -resource "oci_core_route_table" "public" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.workbench.id - display_name = "${local.name}-rt-public" - route_rules { - network_entity_id = oci_core_internet_gateway.workbench.id - destination = "0.0.0.0/0" - destination_type = "CIDR_BLOCK" - } - freeform_tags = local.common_tags -} - -# Workers + LoadBalancer share one /24 — fine for a single-node -# free-tier cluster. The OKE control plane is fully managed; it -# doesn't consume an IP in this VCN. -resource "oci_core_subnet" "public" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.workbench.id - cidr_block = "10.42.0.0/24" - display_name = "${local.name}-subnet-public" - dns_label = "pub" - route_table_id = oci_core_route_table.public.id - prohibit_public_ip_on_vnic = false - freeform_tags = local.common_tags -} - -# Pod CNI subnet — pods get IPs from here under OCI_VCN_IP_NATIVE. -resource "oci_core_subnet" "pods" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.workbench.id - cidr_block = "10.42.1.0/24" - display_name = "${local.name}-subnet-pods" - dns_label = "pods" - route_table_id = oci_core_route_table.public.id - prohibit_public_ip_on_vnic = true - freeform_tags = local.common_tags -} - -# Dedicated worker-node subnet — kept separate from the LB subnet -# because OCI rejects the same subnet appearing in both -# `service_lb_subnet_ids` and node_pool placement_configs ("service -# subnets cannot be used by node pools"). Workers are public so the -# kubelet can reach the OKE API endpoint without a NAT hop. -resource "oci_core_subnet" "workers" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.workbench.id - cidr_block = "10.42.2.0/24" - display_name = "${local.name}-subnet-workers" - dns_label = "workers" - route_table_id = oci_core_route_table.public.id - prohibit_public_ip_on_vnic = false - freeform_tags = local.common_tags -} - -# ------------------------------------------------------------------- -# Network Security Groups — minimal allow-lists. Tight by default; -# loosen only when the workbench needs to expose more than :5173. -# ------------------------------------------------------------------- - -resource "oci_core_network_security_group" "oke_api" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.workbench.id - display_name = "${local.name}-nsg-oke-api" - freeform_tags = local.common_tags -} - -resource "oci_core_network_security_group_security_rule" "oke_api_ingress" { - network_security_group_id = oci_core_network_security_group.oke_api.id - direction = "INGRESS" - protocol = "6" # TCP - source = "0.0.0.0/0" - source_type = "CIDR_BLOCK" - tcp_options { - destination_port_range { - min = 6443 - max = 6443 - } - } -} - -resource "oci_core_network_security_group" "workers" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.workbench.id - display_name = "${local.name}-nsg-workers" - freeform_tags = local.common_tags -} - -# Workers: full egress. -resource "oci_core_network_security_group_security_rule" "workers_egress" { - network_security_group_id = oci_core_network_security_group.workers.id - direction = "EGRESS" - protocol = "all" - destination = "0.0.0.0/0" - destination_type = "CIDR_BLOCK" -} - -# Workers: allow LB → NodePort range so the OCI LB service can -# health-check + forward to the workbench container's port 5173. -resource "oci_core_network_security_group_security_rule" "workers_nodeport_ingress" { - network_security_group_id = oci_core_network_security_group.workers.id - direction = "INGRESS" - protocol = "6" # TCP - source = "10.42.0.0/24" # public subnet (LB lives here) - source_type = "CIDR_BLOCK" - tcp_options { - destination_port_range { - min = 30000 - max = 32767 - } - } -} - -# Workers: pod CNI requires intra-VCN reachability. -resource "oci_core_network_security_group_security_rule" "workers_vcn_ingress" { - network_security_group_id = oci_core_network_security_group.workers.id - direction = "INGRESS" - protocol = "all" - source = var.vcn_cidr - source_type = "CIDR_BLOCK" -} - -# SSH for emergency debugging. Tighten the source CIDR to your -# office IP if you don't want the world knocking on port 22. -resource "oci_core_network_security_group_security_rule" "workers_ssh_ingress" { - network_security_group_id = oci_core_network_security_group.workers.id - direction = "INGRESS" - protocol = "6" - source = "0.0.0.0/0" - source_type = "CIDR_BLOCK" - tcp_options { - destination_port_range { - min = 22 - max = 22 - } - } -} - -# Workers: TCP 10250 ingress from anywhere — the OKE control plane -# reaches back to the kubelet on this port for node registration, -# `kubectl logs`, `kubectl exec`, metrics. Without this rule the -# node never moves to Ready and node-pool create eventually fails -# with "1 nodes(s) register timeout". OKE's control plane lives on -# Oracle's network (not in the VCN), so the source must be 0.0.0.0/0 -# — restricting to the VCN CIDR breaks registration. This matches the -# OCI-recommended OKE worker NSG topology. -resource "oci_core_network_security_group_security_rule" "workers_kubelet_ingress" { - network_security_group_id = oci_core_network_security_group.workers.id - direction = "INGRESS" - protocol = "6" # TCP - source = "0.0.0.0/0" - source_type = "CIDR_BLOCK" - tcp_options { - destination_port_range { - min = 10250 - max = 10250 - } - } -} - -# Workers: ICMP type 3 code 4 — Path MTU Discovery (Fragmentation -# Needed). Without this PMTUD breaks and large packets get black- -# holed. Standard OKE worker-NSG requirement. -resource "oci_core_network_security_group_security_rule" "workers_pmtud_ingress" { - network_security_group_id = oci_core_network_security_group.workers.id - direction = "INGRESS" - protocol = "1" # ICMP - source = "0.0.0.0/0" - source_type = "CIDR_BLOCK" - icmp_options { - type = 3 - code = 4 - } -} diff --git a/deploy/locus-workbench/cimientos/terraform/ocir.tf b/deploy/locus-workbench/cimientos/terraform/ocir.tf deleted file mode 100644 index df025d4f..00000000 --- a/deploy/locus-workbench/cimientos/terraform/ocir.tf +++ /dev/null @@ -1,26 +0,0 @@ -# OCI Container Registry — one repository for the workbench image. -# Region-specific; the registry FQDN is ".ocir.io". -# For ca-toronto-1 that's "yyz.ocir.io". -# -# The Makefile's docker-build-push target tags the local image as: -# yyz.ocir.io//locus-workbench: -# and pushes against an auth token created via the Console for the -# IAM user that runs `make docker-build-push`. - -data "oci_objectstorage_namespace" "ns" { - compartment_id = var.tenancy_ocid -} - -resource "oci_artifacts_container_repository" "workbench" { - compartment_id = var.compartment_ocid - display_name = "locus-workbench" - is_public = false - freeform_tags = local.common_tags - - # OCIR retains repositories across terraform destroy by default; - # this lifecycle hook lets destroy clean up the repo too so the - # tenancy resets cleanly. - lifecycle { - create_before_destroy = false - } -} diff --git a/deploy/locus-workbench/cimientos/terraform/oke.tf b/deploy/locus-workbench/cimientos/terraform/oke.tf deleted file mode 100644 index 68ab0c92..00000000 --- a/deploy/locus-workbench/cimientos/terraform/oke.tf +++ /dev/null @@ -1,93 +0,0 @@ -# OKE cluster — defaults to BASIC_CLUSTER which is the $0/hour -# control-plane SKU on Always-Free. Switch ``type`` to -# ``ENHANCED_CLUSTER`` if your tenancy already has the one BASIC slot -# consumed by another cluster — the free-tier quota is one BASIC -# cluster but up to 15 ENHANCED clusters (which cost $0.10/hr each -# for the control plane). Use ``oci limits resource-availability get -# --service-name container-engine --limit-name cluster-count`` to -# check before applying. -# -# Public endpoint so kubectl works from anywhere with the cluster's -# kubeconfig. Single ARM A1.Flex node provides the entire compute -# envelope for the workbench's 3 tiers. - -data "oci_identity_availability_domains" "ads" { - compartment_id = var.tenancy_ocid -} - -resource "oci_containerengine_cluster" "workbench" { - compartment_id = var.compartment_ocid - vcn_id = oci_core_vcn.workbench.id - kubernetes_version = var.k8s_version - name = "${local.name}-oke" - type = "BASIC_CLUSTER" - - endpoint_config { - is_public_ip_enabled = true - subnet_id = oci_core_subnet.public.id - nsg_ids = [oci_core_network_security_group.oke_api.id] - } - - cluster_pod_network_options { - cni_type = "OCI_VCN_IP_NATIVE" - } - - options { - service_lb_subnet_ids = [oci_core_subnet.public.id] - add_ons { - is_kubernetes_dashboard_enabled = false - is_tiller_enabled = false - } - } - - freeform_tags = local.common_tags - - # OKE clusters cannot be moved between compartments — the OCI API - # has no /clusters/{id}/actions/changeCompartment endpoint. Pin - # the compartment forever or accept a destroy + recreate. - lifecycle { - ignore_changes = [compartment_id] - } -} - -resource "oci_containerengine_node_pool" "workbench" { - cluster_id = oci_containerengine_cluster.workbench.id - compartment_id = var.compartment_ocid - kubernetes_version = var.k8s_version - name = "${local.name}-pool-default" - - node_shape = var.node_pool_shape - node_shape_config { - ocpus = var.node_pool_ocpus - memory_in_gbs = var.node_pool_memory_gb - } - - node_source_details { - source_type = "IMAGE" - image_id = var.node_pool_image_id - boot_volume_size_in_gbs = 50 - } - - node_config_details { - size = var.node_pool_size - placement_configs { - availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name - # Dedicated workers subnet — must be different from the LB subnet - # listed in `options.service_lb_subnet_ids`. OCI rejects subnets - # that appear in both with "service subnets cannot be used by - # node pools". - subnet_id = oci_core_subnet.workers.id - } - nsg_ids = [oci_core_network_security_group.workers.id] - node_pool_pod_network_option_details { - cni_type = "OCI_VCN_IP_NATIVE" - pod_subnet_ids = [oci_core_subnet.pods.id] - } - } - - freeform_tags = local.common_tags - - lifecycle { - ignore_changes = [compartment_id] - } -} diff --git a/deploy/locus-workbench/cimientos/terraform/outputs.tf b/deploy/locus-workbench/cimientos/terraform/outputs.tf deleted file mode 100644 index 24472e62..00000000 --- a/deploy/locus-workbench/cimientos/terraform/outputs.tf +++ /dev/null @@ -1,76 +0,0 @@ -output "cluster_id" { - description = "OKE cluster OCID — feed to `oci ce cluster create-kubeconfig`." - value = oci_containerengine_cluster.workbench.id -} - -output "cluster_name" { - value = oci_containerengine_cluster.workbench.name -} - -output "kubeconfig_command" { - description = "One-shot command to wire kubectl to the new cluster." - value = format( - "oci ce cluster create-kubeconfig --cluster-id %s --file ~/.kube/locus-workbench.kubeconfig --region %s --token-version 2.0.0 --kube-endpoint PUBLIC_ENDPOINT --profile %s", - oci_containerengine_cluster.workbench.id, - var.region, - var.oci_profile, - ) -} - -output "ocir_namespace" { - description = "Tenancy namespace prefix for OCIR image refs." - value = data.oci_objectstorage_namespace.ns.namespace -} - -output "ocir_image_ref" { - description = "Fully-qualified image name to use in the Helm values.yaml." - value = format( - "%s.ocir.io/%s/locus-workbench", - local.region_code, - data.oci_objectstorage_namespace.ns.namespace, - ) -} - -output "vcn_id" { - value = oci_core_vcn.workbench.id -} - -# Vault — the GitHub Actions workflow looks up secrets by name: -# oci secrets secret-bundle get-secret-bundle-by-name \ -# --vault-id --secret-name locus-workbench-ocir-auth-token -# So all the workflow needs at runtime is the vault OCID. -output "vault_id" { - description = "OCI Vault holding the workbench runtime secrets." - value = oci_kms_vault.workbench.id -} - -output "vault_secret_names" { - description = "Names of the secrets the workflow expects to read from the vault." - value = [for s in oci_vault_secret.workbench : s.secret_name] -} - -# Set each secret post-apply with: -# oci vault secret update-base64 --secret-id --secret-content-content "$(printf 'real-value' | base64)" -output "vault_secret_set_commands" { - description = "Per-secret one-liners to set the real value after first apply." - value = { - for k, s in oci_vault_secret.workbench : - k => "oci vault secret update-base64 --secret-id ${s.id} --secret-content-content \"$(printf 'YOUR_VALUE' | base64)\"" - } - sensitive = false -} - -# Region-to-OCIR-prefix map for the few regions Free Tier commonly -# lands in. Extend as needed. -locals { - region_code_by_region = { - "ca-toronto-1" = "yyz" - "us-phoenix-1" = "phx" - "us-ashburn-1" = "iad" - "us-chicago-1" = "ord" - "eu-frankfurt-1" = "fra" - "uk-london-1" = "lhr" - "sa-saopaulo-1" = "gru" - } - region_code = lookup(local.region_code_by_region, var.region, var.region) -} diff --git a/deploy/locus-workbench/cimientos/terraform/providers.tf b/deploy/locus-workbench/cimientos/terraform/providers.tf deleted file mode 100644 index 7377d65f..00000000 --- a/deploy/locus-workbench/cimientos/terraform/providers.tf +++ /dev/null @@ -1,18 +0,0 @@ -# Terraform provider configuration for the locus-workbench -# free-tier stack. Defaults to the OCI Always-Free home region in -# Toronto; override `region` in terraform.tfvars to retarget. - -terraform { - required_version = ">= 1.5.0" - required_providers { - oci = { - source = "oracle/oci" - version = ">= 5.45.0" - } - } -} - -provider "oci" { - config_file_profile = var.oci_profile - region = var.region -} diff --git a/deploy/locus-workbench/cimientos/terraform/terraform.tfvars.example b/deploy/locus-workbench/cimientos/terraform/terraform.tfvars.example deleted file mode 100644 index 0adfab90..00000000 --- a/deploy/locus-workbench/cimientos/terraform/terraform.tfvars.example +++ /dev/null @@ -1,24 +0,0 @@ -# Copy to terraform.tfvars and fill in the two OCIDs. -# All other vars have free-tier-safe defaults. - -# Tenancy OCID — find with: -# oci iam tenancy get --tenancy-id $(oci iam compartment list --query 'data[0]."tenancy-id"' --raw-output --profile API_FREE_TIER) --profile API_FREE_TIER -tenancy_ocid = "ocid1.tenancy.oc1..xxxxxxxx" - -# Compartment to provision into. Use the tenancy root OCID for the -# simplest free-tier setup (no IAM policy required). -compartment_ocid = "ocid1.tenancy.oc1..xxxxxxxx" - -# Worker image OCID for the chosen k8s_version + region. Pull with: -# oci ce node-pool-options get --node-pool-option-id all \ -# --profile API_FREE_TIER --region ca-toronto-1 \ -# --query 'data.sources[?contains(\"source-name\", `OKE-1.31`) && contains(\"source-name\", `aarch64`)] | [0]."image-id"' -# (Free Tier in YYZ as of 2026-05; bump when k8s_version moves.) -node_pool_image_id = "ocid1.image.oc1.ca-toronto-1.xxxxxxxx" - -# Optional overrides — uncomment to deviate from defaults. -# region = "ca-toronto-1" -# oci_profile = "API_FREE_TIER" -# node_pool_ocpus = 2 -# node_pool_memory_gb = 12 -# node_pool_size = 1 diff --git a/deploy/locus-workbench/cimientos/terraform/variables.tf b/deploy/locus-workbench/cimientos/terraform/variables.tf deleted file mode 100644 index 2fe0f3ee..00000000 --- a/deploy/locus-workbench/cimientos/terraform/variables.tf +++ /dev/null @@ -1,80 +0,0 @@ -# Inputs. Set in terraform.tfvars (see terraform.tfvars.example) or -# pass with -var on the CLI. All have sensible Always-Free defaults -# for ca-toronto-1 so a fresh apply works with zero overrides on a -# blank free-tier tenancy. - -variable "tenancy_ocid" { - description = "Tenancy OCID. The free-tier home tenancy." - type = string -} - -variable "compartment_ocid" { - description = "Compartment OCID to provision into. Tenancy root works on free tier." - type = string -} - -variable "oci_profile" { - description = "Profile name in ~/.oci/config. Default API_FREE_TIER." - type = string - default = "API_FREE_TIER" -} - -variable "region" { - description = "Home region of the free-tier tenancy. Default ca-toronto-1." - type = string - default = "ca-toronto-1" -} - -variable "name_prefix" { - description = "Prefix for every resource name." - type = string - default = "locus-workbench" -} - -variable "vcn_cidr" { - description = "CIDR for the workbench VCN." - type = string - default = "10.42.0.0/16" -} - -variable "k8s_version" { - description = "OKE Kubernetes version. Defaults to a current LTS line." - type = string - default = "v1.32.10" -} - -variable "node_pool_shape" { - description = "Worker node shape. ARM A1.Flex is the only Always-Free shape." - type = string - default = "VM.Standard.A1.Flex" -} - -variable "node_pool_ocpus" { - description = "OCPUs per worker. Free-tier budget is 4 OCPU total across A1.Flex." - type = number - default = 2 -} - -variable "node_pool_memory_gb" { - description = "Memory per worker in GB. Free-tier budget is 24 GB total." - type = number - default = 12 -} - -variable "node_pool_size" { - description = "Number of worker nodes. 1 fits everything within free-tier limits." - type = number - default = 1 -} - -variable "node_pool_image_id" { - description = <<-EOT - Oracle Linux 8 ARM image OCID for the OKE node pool. Region-specific. - Pull the latest with: - oci ce node-pool-options get --node-pool-option-id all \ - --profile API_FREE_TIER --region ca-toronto-1 \ - --query 'data.sources[?contains(\"source-name\", `OKE-1.31.10`) && contains(\"source-name\", `aarch64`)]' - EOT - type = string - default = "" -} diff --git a/deploy/locus-workbench/cimientos/terraform/vault.tf b/deploy/locus-workbench/cimientos/terraform/vault.tf deleted file mode 100644 index 66bfcff9..00000000 --- a/deploy/locus-workbench/cimientos/terraform/vault.tf +++ /dev/null @@ -1,70 +0,0 @@ -# OCI Vault — runtime secret store. Terraform creates the vault, -# master key, and 6 secret containers (placeholders). Actual values -# are populated post-apply via `oci vault secret update-base64` so -# rotations don't drift Terraform state. -# -# Free-tier allowance: -# 1 vault (DEFAULT type) -# 20 key versions -# 150 secret versions -# We use 1 vault + 1 key + 6 secrets — well within budget. - -resource "oci_kms_vault" "workbench" { - compartment_id = var.compartment_ocid - display_name = "${local.name}-vault" - vault_type = "DEFAULT" - freeform_tags = local.common_tags -} - -resource "oci_kms_key" "workbench_master" { - compartment_id = var.compartment_ocid - display_name = "${local.name}-master-key" - protection_mode = "SOFTWARE" - management_endpoint = oci_kms_vault.workbench.management_endpoint - - key_shape { - algorithm = "AES" - length = 32 - } - freeform_tags = local.common_tags -} - -# The six secrets the GitHub Actions workflow needs at deploy time. -# Names match the env vars the workflow expects so the lookup loop -# is trivial. -locals { - workbench_secret_names = [ - "ocir-auth-token", # password for docker login to OCIR - "ocir-username", # OCI Console username (no namespace prefix) - "s3compat-access-key", # Customer Secret Key access part — TF state backend - "s3compat-secret-key", # Customer Secret Key secret part — TF state backend - "openai-api-key", # injected into workbench pods (optional) - "anthropic-api-key", # injected into workbench pods (optional) - ] -} - -resource "oci_vault_secret" "workbench" { - for_each = toset(local.workbench_secret_names) - compartment_id = var.compartment_ocid - vault_id = oci_kms_vault.workbench.id - key_id = oci_kms_key.workbench_master.id - secret_name = "${local.name}-${each.key}" - description = "locus-workbench runtime secret: ${each.key}" - - # Initial content is a base64 sentinel — real values land via: - # oci vault secret update-base64 \ - # --secret-id --secret-content-content "$(printf 'real-value' | base64)" - secret_content { - content_type = "BASE64" - content = base64encode("placeholder-set-me-with-update-base64") - } - - lifecycle { - # Once a real value lands, Terraform must NOT overwrite it on - # the next apply. Rotation flows through `oci vault secret - # update-base64` directly, not through state. - ignore_changes = [secret_content] - } - - freeform_tags = local.common_tags -} diff --git a/deploy/locus-workbench/helm/locus-workbench/Chart.yaml b/deploy/locus-workbench/helm/locus-workbench/Chart.yaml deleted file mode 100644 index 8e0708d7..00000000 --- a/deploy/locus-workbench/helm/locus-workbench/Chart.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v2 -name: locus-workbench -description: Browser playground for every locus pattern. Single-pod deployment of the three workbench tiers (FastAPI runner, Express BFF, Vite web). -type: application -version: 0.1.0 -appVersion: 0.2.0b9 -keywords: -- locus -- workbench -- oracle -- oke -- free-tier -sources: -- https://github.com/oracle-samples/locus -maintainers: -- name: Federico Kamelhar diff --git a/deploy/locus-workbench/helm/locus-workbench/templates/NOTES.txt b/deploy/locus-workbench/helm/locus-workbench/templates/NOTES.txt deleted file mode 100644 index 8d69e525..00000000 --- a/deploy/locus-workbench/helm/locus-workbench/templates/NOTES.txt +++ /dev/null @@ -1,22 +0,0 @@ -locus-workbench {{ .Chart.AppVersion }} deployed. - -1. Watch the rollout: - kubectl --kubeconfig ~/.kube/locus-workbench.kubeconfig \ - rollout status deployment/{{ include "locus-workbench.fullname" . }} \ - --namespace {{ .Release.Namespace }} --timeout=5m - -2. Wait for the LoadBalancer to get a public IP: - kubectl --kubeconfig ~/.kube/locus-workbench.kubeconfig \ - get svc {{ include "locus-workbench.fullname" . }} \ - --namespace {{ .Release.Namespace }} \ - -o jsonpath='{.status.loadBalancer.ingress[0].ip}' - -3. Open the URL the previous command prints in your browser. - -4. In Provider settings, paste an OpenAI or Anthropic API key, then - pick a tutorial and hit Run. - -If the LoadBalancer never gets an IP, OCI may have run out of -free Flex 10 Mbps load balancers in this region. Check: - kubectl describe svc {{ include "locus-workbench.fullname" . }} \ - --namespace {{ .Release.Namespace }} diff --git a/deploy/locus-workbench/helm/locus-workbench/templates/_helpers.tpl b/deploy/locus-workbench/helm/locus-workbench/templates/_helpers.tpl deleted file mode 100644 index 0e91dfa3..00000000 --- a/deploy/locus-workbench/helm/locus-workbench/templates/_helpers.tpl +++ /dev/null @@ -1,45 +0,0 @@ -{{/* -Common template helpers — name, fullname, labels, selectorLabels. -Pattern lifted from `helm create` defaults. -*/}} - -{{- define "locus-workbench.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "locus-workbench.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 "locus-workbench.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{- define "locus-workbench.labels" -}} -helm.sh/chart: {{ include "locus-workbench.chart" . }} -{{ include "locus-workbench.selectorLabels" . }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end -}} - -{{- define "locus-workbench.selectorLabels" -}} -app.kubernetes.io/name: {{ include "locus-workbench.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end -}} - -{{- define "locus-workbench.serviceAccountName" -}} -{{- if .Values.serviceAccount.create -}} -{{- default (include "locus-workbench.fullname" .) .Values.serviceAccount.name -}} -{{- else -}} -{{- default "default" .Values.serviceAccount.name -}} -{{- end -}} -{{- end -}} diff --git a/deploy/locus-workbench/helm/locus-workbench/templates/deployment.yaml b/deploy/locus-workbench/helm/locus-workbench/templates/deployment.yaml deleted file mode 100644 index 610396e5..00000000 --- a/deploy/locus-workbench/helm/locus-workbench/templates/deployment.yaml +++ /dev/null @@ -1,95 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "locus-workbench.fullname" . }} - labels: - {{- include "locus-workbench.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - {{- include "locus-workbench.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - {{- include "locus-workbench.selectorLabels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - serviceAccountName: {{ include "locus-workbench.serviceAccountName" . }} - {{- with .Values.podSecurityContext }} - securityContext: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - containers: - - name: workbench - image: "{{ required "image.repository must be set (OCIR ref from `terraform output -raw ocir_image_ref`)" .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - {{- with .Values.command }} - command: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.args }} - args: - {{- toYaml . | nindent 8 }} - {{- end }} - ports: - # The Service targets 5173 directly. The other two ports are - # internal-only (BFF + backend reach each other via localhost) - # but we expose them so `kubectl port-forward` can hit any - # tier directly for debugging. - - name: web - containerPort: 5173 - protocol: TCP - - name: bff - containerPort: 3101 - protocol: TCP - - name: backend - containerPort: 8100 - protocol: TCP - {{- with .Values.env }} - env: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- if .Values.envFromSecret.name }} - envFrom: - - secretRef: - name: {{ .Values.envFromSecret.name }} - {{- end }} - {{- with .Values.resources }} - resources: - {{- toYaml . | nindent 10 }} - {{- end }} - {{- with .Values.livenessProbe }} - livenessProbe: - {{- toYaml . | nindent 10 }} - {{- end }} - {{- with .Values.readinessProbe }} - readinessProbe: - {{- toYaml . | nindent 10 }} - {{- end }} - {{- with .Values.securityContext }} - securityContext: - {{- toYaml . | nindent 10 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/deploy/locus-workbench/helm/locus-workbench/templates/ingress.yaml b/deploy/locus-workbench/helm/locus-workbench/templates/ingress.yaml deleted file mode 100644 index 2d106230..00000000 --- a/deploy/locus-workbench/helm/locus-workbench/templates/ingress.yaml +++ /dev/null @@ -1,31 +0,0 @@ -{{- if .Values.ingress.enabled -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ include "locus-workbench.fullname" . }} - labels: - {{- include "locus-workbench.labels" . | nindent 4 }} -spec: - {{- with .Values.ingress.className }} - ingressClassName: {{ . }} - {{- end }} - {{- with .Values.ingress.tls }} - tls: - {{- toYaml . | nindent 4 }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - pathType: {{ .pathType }} - backend: - service: - name: {{ include "locus-workbench.fullname" $ }} - port: - number: {{ $.Values.service.port }} - {{- end }} - {{- end }} -{{- end }} diff --git a/deploy/locus-workbench/helm/locus-workbench/templates/service.yaml b/deploy/locus-workbench/helm/locus-workbench/templates/service.yaml deleted file mode 100644 index b4645c04..00000000 --- a/deploy/locus-workbench/helm/locus-workbench/templates/service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ include "locus-workbench.fullname" . }} - labels: - {{- include "locus-workbench.labels" . | nindent 4 }} - {{- with .Values.service.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: {{ .Values.service.targetPort }} - protocol: TCP - name: web - selector: - {{- include "locus-workbench.selectorLabels" . | nindent 4 }} diff --git a/deploy/locus-workbench/helm/locus-workbench/templates/serviceaccount.yaml b/deploy/locus-workbench/helm/locus-workbench/templates/serviceaccount.yaml deleted file mode 100644 index 0efb68f1..00000000 --- a/deploy/locus-workbench/helm/locus-workbench/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "locus-workbench.serviceAccountName" . }} - labels: - {{- include "locus-workbench.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/deploy/locus-workbench/helm/locus-workbench/values.yaml b/deploy/locus-workbench/helm/locus-workbench/values.yaml deleted file mode 100644 index a976e281..00000000 --- a/deploy/locus-workbench/helm/locus-workbench/values.yaml +++ /dev/null @@ -1,108 +0,0 @@ -# locus-workbench Helm values — Always-Free OKE defaults. -# Override on the CLI: -# helm install locus-workbench ./helm/locus-workbench \ -# --set image.repository=yyz.ocir.io//locus-workbench \ -# --set image.tag=0.2.0b9 - -image: - # Set to the OCIR ref printed by `terraform output -raw ocir_image_ref`. - repository: '' - tag: 0.2.0b9 - # IfNotPresent works for tagged releases; switch to Always while - # iterating on a `latest` tag. - pullPolicy: IfNotPresent - -# OCIR pull secret. Created by the Makefile target `make ocir-secret` -# from an auth token. Leave empty if the OKE node pool already has -# an instance principal policy granting OCIR read. -imagePullSecrets: -- name: ocir-pull-secret - -replicaCount: 1 - -service: - # LoadBalancer surfaces the workbench at a public IP. Flex 10 Mbps - # is free on OCI's Always-Free tier. - type: LoadBalancer - port: 80 - targetPort: 5173 - annotations: - # Force the 10 Mbps free-tier Flex shape. Any non-Always-Free - # shape silently bills. - service.beta.kubernetes.io/oci-load-balancer-shape: flexible - service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: '10' - service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: '10' - -# Optional: bind a real hostname via an Ingress (NGINX or OCI Native -# Ingress). Disabled by default — LoadBalancer + IP is enough for a -# demo deployment. -ingress: - enabled: false - className: '' - hosts: - - host: workbench.example.com - paths: - - path: / - pathType: Prefix - tls: [] - -resources: - # Free-tier worker has 2 OCPU + 12 GB. We give the workbench pod a - # generous slice — the Vite dev server alone uses ~700 MB; the OCI - # SDK pulls in ~200 MB more on cold import. - requests: - cpu: 500m - memory: 1Gi - limits: - cpu: 1500m - memory: 3Gi - -# The container CMD already starts all 3 tiers. Override the -# arg list only if you need to disable a tier (e.g. backend-only). -command: [] -args: [] - -env: - # Everything below is exposed inside the running container as a - # plain env var. Keep API keys in the externally-managed secret - # (see `envFromSecret`) rather than checking them in here. -- name: LOCUS_MODEL_PROVIDER - value: openai # users can override via Provider settings in the UI - -envFromSecret: - # Helm renders this into an `envFrom: - secretRef: name: ` - # block when set. Create the secret with: - # kubectl create secret generic locus-workbench-keys \ - # --from-literal=OPENAI_API_KEY=sk-... \ - # --from-literal=ANTHROPIC_API_KEY=sk-ant-... - name: '' - -# Liveness + readiness on the Vite-facing port 5173. -livenessProbe: - httpGet: - path: / - port: 5173 - initialDelaySeconds: 60 - periodSeconds: 30 - timeoutSeconds: 5 - failureThreshold: 5 -readinessProbe: - httpGet: - path: / - port: 5173 - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 5 - -serviceAccount: - create: true - name: '' - annotations: {} - -podAnnotations: {} -podLabels: {} -nodeSelector: {} -tolerations: [] -affinity: {} -securityContext: {} -podSecurityContext: {} diff --git a/deploy/locus-workbench/scripts/bootstrap-bucket.sh b/deploy/locus-workbench/scripts/bootstrap-bucket.sh deleted file mode 100755 index f91073ef..00000000 --- a/deploy/locus-workbench/scripts/bootstrap-bucket.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# One-time bootstrap for the Terraform state bucket. Creates an OCI -# Object Storage bucket and prints the namespace-scoped S3-compat -# endpoint you'll pass to `terraform init`. -# -# Usage: -# ./scripts/bootstrap-bucket.sh -# -# Idempotent — re-runs are safe; it skips create if the bucket -# already exists. - -set -euo pipefail - -PROFILE="${OCI_PROFILE:-API_FREE_TIER}" -REGION="${OCI_REGION:-ca-toronto-1}" -COMPARTMENT_ID="${OCI_COMPARTMENT_OCID:?must set OCI_COMPARTMENT_OCID}" -BUCKET_NAME="${BUCKET_NAME:-locus-workbench-tfstate}" - -echo "Resolving namespace..." -NAMESPACE=$( - oci os ns get \ - --profile "$PROFILE" --region "$REGION" \ - --auth api_key \ - --query 'data' --raw-output -) -echo "Namespace: $NAMESPACE" - -ENDPOINT="https://$NAMESPACE.compat.objectstorage.$REGION.oraclecloud.com" -echo "S3-compat endpoint: $ENDPOINT" - -if oci os bucket get \ - --profile "$PROFILE" --region "$REGION" --auth api_key \ - --bucket-name "$BUCKET_NAME" \ - --namespace-name "$NAMESPACE" \ - >/dev/null 2>&1; then - echo "Bucket $BUCKET_NAME already exists — skipping create." -else - echo "Creating bucket $BUCKET_NAME..." - oci os bucket create \ - --profile "$PROFILE" --region "$REGION" --auth api_key \ - --compartment-id "$COMPARTMENT_ID" \ - --namespace-name "$NAMESPACE" \ - --name "$BUCKET_NAME" \ - --versioning Enabled \ - --public-access-type NoPublicAccess -fi - -cat < list[dict]: - """Search the GDS for available flights.""" - # Stub. Wire to your real flight API. - return [ - {"flight_id": "AA-181", "origin": origin, "destination": destination, "date": date}, - ] - - -@tool(idempotent=True) -def book_flight(flight_id: str, customer_id: str) -> dict: - """Book a flight. Re-fires return the cached receipt — never double-charge.""" - # Stub. Wire to your real billing system. - return {"confirmation": "BK-58291", "flight_id": flight_id} - - -# --------------------------------------------------------------------------- -# Checkpointer — durable threads in OCI Object Storage. -# --------------------------------------------------------------------------- -checkpointer = OCIBucketBackend( - bucket_name=os.environ["LOCUS_OCI_BUCKET_NAME"], - namespace=os.environ["LOCUS_OCI_NAMESPACE"], - prefix=os.environ.get("LOCUS_OCI_BUCKET_PREFIX", "locus/threads/"), - auth_type=os.environ.get("OCI_AUTH_TYPE", "api_key"), -) - - -# --------------------------------------------------------------------------- -# Agent. -# --------------------------------------------------------------------------- -agent = Agent( - model=os.environ.get("LOCUS_MODEL", "oci:openai.gpt-5"), - tools=[search_flights, book_flight], - system_prompt="You are a travel concierge. Find a flight, then book it.", - checkpointer=checkpointer, -) - - -# --------------------------------------------------------------------------- -# Server. Bearer-token auth + per-principal thread isolation. -# --------------------------------------------------------------------------- -server = AgentServer( - agent=agent, - api_key=os.environ.get("LOCUS_SERVER_API_KEY"), - title="Travel Concierge", -) - - -# Module-level export for uvicorn: -# uvicorn app:server.app --host 0.0.0.0 --port 8080 -app = server.app diff --git a/docs/how-to/deploy-workbench-free-tier.md b/docs/how-to/deploy-workbench-free-tier.md deleted file mode 100644 index a6c690cf..00000000 --- a/docs/how-to/deploy-workbench-free-tier.md +++ /dev/null @@ -1,180 +0,0 @@ -# How-to: Deploy the workbench on OCI Always-Free OKE - -Stand up the workbench as a Kubernetes pod inside your OCI -Always-Free tenancy in ~15 minutes. Single ARM A1.Flex worker, free -Flex 10 Mbps load balancer, free OCIR repository. Everything stays -within the Always-Free envelope so you pay $0/month. - -## What you'll have when this is done - -```text -Internet - │ - ▼ -Flex 10 Mbps LoadBalancer (free) - │ - ▼ port 80 → 5173 -ARM A1.Flex worker node (2 OCPU, 12 GB, free) - └── pod: locus-workbench- - ├── tier 1 : Vite dev server :5173 ← public - ├── tier 2 : Express BFF :3101 ← internal - └── tier 3 : FastAPI runner :8100 ← internal -``` - -One Deployment, one Service, one image — the workbench's existing -`Dockerfile` already bundles all three tiers into a single CMD. - -## Prerequisites - -- An OCI tenancy with the **Always-Free** allocation available - (4 ARM OCPU + 24 GB RAM unspent, one Flex LB free, one OKE cluster free). -- `terraform` (≥ 1.6), `kubectl`, `helm`, `docker`, and `oci` CLI on - your laptop. -- An OCI config profile pointing at the free tenancy. The repo's - Makefile defaults to `~/.oci/config` profile `API_FREE_TIER`. -- An OCI Auth Token for OCIR — generate at - *User → Auth Tokens → Generate Token* in the Console. - -## Stack contents - -| Path | What it provisions | -|---|---| -| [`deploy/locus-workbench/cimientos/terraform/`](https://github.com/oracle-samples/locus/tree/main/deploy/locus-workbench/cimientos/terraform) | VCN (`10.42.0.0/16`), IGW, public + pod subnets, NSGs for OKE API + workers, OKE cluster (BASIC default), ARM A1.Flex node pool, OCIR repo, KMS Vault + KMS Key + 6 secret containers | -| [`deploy/locus-workbench/scripts/`](https://github.com/oracle-samples/locus/tree/main/deploy/locus-workbench/scripts) | `bootstrap-bucket.sh` for the one-time TF state bucket setup (S3-compat remote backend) | -| [`deploy/locus-workbench/helm/locus-workbench/`](https://github.com/oracle-samples/locus/tree/main/deploy/locus-workbench/helm/locus-workbench) | Deployment (one container, all three tiers), LoadBalancer Service annotated for Flex 10 Mbps, ServiceAccount, optional Ingress | -| [`deploy/locus-workbench/Makefile`](https://github.com/oracle-samples/locus/tree/main/deploy/locus-workbench/Makefile) | `tf-apply`, `kubeconfig`, `ocir-login`, `docker-push`, `ocir-secret`, `helm-install`, `url`, `destroy` | - -## Step 1 — Fill in the two OCIDs - -```bash -cd deploy/locus-workbench -cp cimientos/terraform/terraform.tfvars.example cimientos/terraform/terraform.tfvars -``` - -Edit `cimientos/terraform/terraform.tfvars` and replace the placeholders: - -- `tenancy_ocid` — your Free-Tier tenancy OCID -- `compartment_ocid` — same as tenancy for the simplest setup (no - IAM policy required), or a dedicated compartment if you want to - scope billing -- `node_pool_image_id` — Oracle Linux 8 ARM image OCID for the - current OKE version. Pull it with: - -```bash -oci ce node-pool-options get --node-pool-option-id all \ - --profile API_FREE_TIER --region ca-toronto-1 \ - --query 'data.sources[?contains("source-name", `OKE-1.31`) && contains("source-name", `aarch64`)] | [0]."image-id"' \ - --raw-output -``` - -## Step 2 — Provision the cluster - -```bash -make tf-apply -``` - -Takes ~10 minutes on a clean tenancy. When done: - -```bash -make tf-output -# Lists cluster_id, ocir_image_ref, ocir_namespace, vcn_id. -``` - -## Step 3 — Wire kubectl - -```bash -make kubeconfig -``` - -Writes `~/.kube/locus-workbench.kubeconfig` and prints the node -list to confirm the API endpoint is reachable. - -## Step 4 — Push the image to OCIR - -```bash -make ocir-login -# Prompts for username and password: -# username: / -# password: the Auth Token from Step 0 (NOT your console password) - -make docker-push -# Builds workbench/Dockerfile, tags it as -# .ocir.io//locus-workbench:0.2.0b9, pushes both -# the version tag and `latest`. -``` - -First build is ~5 minutes (~1.3 GB image). Subsequent pushes only -ship changed layers. - -## Step 5 — Deploy the Helm chart - -```bash -make ocir-secret # creates the cluster-side image-pull secret -make helm-install # rolls out the Deployment + LoadBalancer -``` - -`helm-install` runs `--wait --timeout=10m`, so it blocks until the -pod is Ready. - -## Step 6 — Open it - -```bash -make url -# http://203.0.113.42 -``` - -Hit that URL in your browser. The workbench lands on the Tutorials -tab. Click **Provider settings** in the header, paste an OpenAI or -Anthropic key (or your free-tier OCI profile), pick a tutorial, hit -Run. - -## Iterating - -```bash -# After changing anything under workbench/ on your laptop: -make docker-push # rebuilds + pushes -make IMAGE_TAG=0.2.0b10 deploy # rolls the cluster onto the new tag -make logs # tail the pod -``` - -## Tearing it all down - -```bash -make destroy -``` - -Type `yes` at the confirm prompt. Removes the Helm release first, -then `terraform destroy` removes the cluster, node pool, OCIR repo, -VCN, and all subnets. State file stays in -`cimientos/terraform/terraform.tfstate` until you delete it. - -## Cost - -| Resource | Free allowance | This stack uses | $/month | -|---|---|---|---| -| OKE BASIC cluster control plane | 1 cluster | 1 | $0 | -| Worker compute (ARM A1.Flex) | 4 OCPU + 24 GB | 2 OCPU + 12 GB | $0 | -| Flex 10 Mbps load balancer | 1 | 1 | $0 | -| OCIR storage | 1 GB | ~600 MB | $0 | -| Egress | 10 TB | well under | $0 | - -The Terraform stack pins every billable knob to the Always-Free -allocation. The Helm chart annotates the Service with -`oci-load-balancer-shape-flex-min: 10` and `-max: 10` so the LB -stays on the free shape — without those annotations the OCI CCM -silently provisions a paid Flex LB. - -## Troubleshooting - -| Symptom | Cause + fix | -|---|---| -| `terraform apply` fails at `oci_containerengine_cluster` with "ServiceLimitExceeded" | You already have an OKE cluster. Delete it or pick a different tenancy. | -| `make docker-push` returns "denied: requested access to the resource is denied" | OCIR Auth Token expired or wrong username format. Re-run `make ocir-login` with `/`. | -| Pod stuck in `ImagePullBackOff` | The pull secret references the wrong registry. Re-run `make ocir-secret` after `make ocir-login`. | -| LoadBalancer Service stays at `` IP | Free-tier Flex LB quota exhausted (1 per region). Delete an old LB or wait. | -| Workbench UI loads but `/api/*` calls return 502 | Backend tier crashed inside the pod. `make logs` to read uvicorn's traceback. | - -## See also - -- [Workbench guide](../workbench.md) — every pattern + tab the UI ships with -- [Cognitive routing pattern](../workbench.md#cognitive-routing-pattern) — the new opt-in LLM picker toggle, live in this deploy diff --git a/docs/how-to/deploy.md b/docs/how-to/deploy.md index 10696f2d..153d476d 100644 --- a/docs/how-to/deploy.md +++ b/docs/how-to/deploy.md @@ -106,35 +106,14 @@ Set `OCI_AUTH_TYPE=instance_principal` in the container env. ## OKE — Kubernetes for production Best for multi-replica, autoscaled, multi-region production. The -quickest path is the **bundled Helm chart** at -[`deploy/helm/locus-agent/`](https://github.com/oracle-samples/locus/tree/main/deploy/helm/locus-agent): - -```bash -helm install locus-agent ./deploy/helm/locus-agent \ - --set image.repository=iad.ocir.io/$NAMESPACE/locus-concierge \ - --set image.tag=0.1.0 \ - --set auth.apiKey=$(openssl rand -hex 16) \ - --set ociBucket.enabled=true \ - --set ociBucket.bucketName=locus-threads-prod \ - --set ociBucket.namespace=$NAMESPACE \ - --set autoscaling.enabled=true \ - --set autoscaling.minReplicas=2 \ - --set autoscaling.maxReplicas=10 -``` - -The chart ships a Deployment, Service, ServiceAccount (with workload- -identity annotation hooks), Secret, HPA, and Ingress, all driven by -`values.yaml`. See [`deploy/helm/locus-agent/README.md`](https://github.com/oracle-samples/locus/blob/main/deploy/helm/locus-agent/README.md) -for the full value reference. - -The container image is built from the [root `Dockerfile`](https://github.com/oracle-samples/locus/blob/main/Dockerfile) — multi-stage, non-root user, `HEALTHCHECK` on `/health`. Build it with: +container image is built from the [root `Dockerfile`](https://github.com/oracle-samples/locus/blob/main/Dockerfile) — multi-stage, non-root user, `HEALTHCHECK` on `/health`. Build it with: ```bash docker build -t iad.ocir.io/$NAMESPACE/locus-concierge:0.1.0 . docker push iad.ocir.io/$NAMESPACE/locus-concierge:0.1.0 ``` -If you need raw YAML instead of Helm, the equivalent is: +A minimal Kubernetes deployment looks like: ```yaml apiVersion: apps/v1 diff --git a/mkdocs.yml b/mkdocs.yml index d39edf24..b0d8f7b5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -249,7 +249,6 @@ nav: - Add a checkpointer backend: how-to/custom-checkpointer.md - OCI GenAI models: how-to/oci-models.md - OCI Dedicated AI Cluster (DAC): how-to/oci-dac.md - - Deploy workbench on Always-Free OKE: how-to/deploy-workbench-free-tier.md - Workbench: workbench.md - API reference: - Agent: api/agent.md diff --git a/tests/unit/test_deploy_helm_chart.py b/tests/unit/test_deploy_helm_chart.py deleted file mode 100644 index c4da89de..00000000 --- a/tests/unit/test_deploy_helm_chart.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Smoke tests for the deploy/helm/locus-agent chart and the root Dockerfile. - -These don't run helm or docker. They validate: -- All required chart files exist with the right shape. -- `Chart.yaml` has the canonical fields. -- `values.yaml` parses and contains the keys the templates reference. -- The Dockerfile contains the expected stages and HEALTHCHECK. - -If `helm` is available in PATH, also runs `helm lint` and `helm template`. -""" - -from __future__ import annotations - -import shutil -import subprocess -from pathlib import Path - -import pytest - - -REPO_ROOT = Path(__file__).resolve().parents[2] -CHART_DIR = REPO_ROOT / "deploy" / "helm" / "locus-agent" -DOCKERFILE = REPO_ROOT / "Dockerfile" - - -REQUIRED_CHART_FILES = ( - "Chart.yaml", - "values.yaml", - ".helmignore", - "README.md", - "templates/_helpers.tpl", - "templates/deployment.yaml", - "templates/service.yaml", - "templates/serviceaccount.yaml", - "templates/secret.yaml", - "templates/hpa.yaml", - "templates/ingress.yaml", -) - - -def test_dockerfile_exists_at_repo_root(): - assert DOCKERFILE.exists(), f"Dockerfile missing at {DOCKERFILE}" - - -def test_dockerfile_has_multistage_build(): - body = DOCKERFILE.read_text() - assert "AS builder" in body, "expected named 'builder' stage" - assert "AS runtime" in body, "expected named 'runtime' stage" - - -def test_dockerfile_uses_non_root_user(): - body = DOCKERFILE.read_text() - assert "useradd" in body, "Dockerfile should create a non-root user" - assert "USER locus" in body, "Dockerfile should drop privileges to that user" - - -def test_dockerfile_has_healthcheck(): - body = DOCKERFILE.read_text() - assert "HEALTHCHECK" in body - assert "/health" in body - - -def test_dockerfile_installs_server_extras(): - body = DOCKERFILE.read_text() - # `[oci,server,checkpoints]` covers production deployment basics. - assert "[oci,server,checkpoints]" in body - - -def test_chart_directory_exists(): - assert CHART_DIR.is_dir(), f"chart dir missing at {CHART_DIR}" - - -@pytest.mark.parametrize("rel_path", REQUIRED_CHART_FILES) -def test_chart_has_required_files(rel_path: str): - path = CHART_DIR / rel_path - assert path.exists(), f"chart file missing: {rel_path}" - - -def test_chart_yaml_has_canonical_fields(): - yaml = pytest.importorskip("yaml") - data = yaml.safe_load((CHART_DIR / "Chart.yaml").read_text()) - assert data["apiVersion"] == "v2" - assert data["name"] == "locus-agent" - assert data["type"] == "application" - assert "version" in data - assert "appVersion" in data - - -def test_values_yaml_parses_with_expected_keys(): - yaml = pytest.importorskip("yaml") - values = yaml.safe_load((CHART_DIR / "values.yaml").read_text()) - - # Top-level sections referenced by the templates. - for key in ( - "image", - "replicaCount", - "auth", - "serviceAccount", - "resources", - "probes", - "service", - "ingress", - "autoscaling", - "ociBucket", - ): - assert key in values, f"values.yaml missing top-level key: {key}" - - # Auth shape. - assert "secretKey" in values["auth"] - # Probes have liveness + readiness + startup. - for probe in ("liveness", "readiness", "startup"): - assert probe in values["probes"], f"probes.{probe} missing" - - -@pytest.mark.skipif(shutil.which("helm") is None, reason="helm CLI not available") -def test_helm_lint_passes(): - """If helm is on PATH, run `helm lint` against the chart.""" - helm = shutil.which("helm") - assert helm is not None # narrowed by skipif; helps the type checker - result = subprocess.run( # noqa: S603 — args fully controlled, helm path resolved - [helm, "lint", str(CHART_DIR)], - capture_output=True, - text=True, - check=False, - ) - assert result.returncode == 0, ( - f"helm lint failed:\n--- stdout ---\n{result.stdout}\n--- stderr ---\n{result.stderr}" - ) - - -@pytest.mark.skipif(shutil.which("helm") is None, reason="helm CLI not available") -def test_helm_template_renders(): - """If helm is on PATH, render templates with default values.""" - helm = shutil.which("helm") - assert helm is not None - result = subprocess.run( # noqa: S603 — args fully controlled, helm path resolved - [ - helm, - "template", - "test-release", - str(CHART_DIR), - "--set", - "auth.apiKey=dummy", - ], - capture_output=True, - text=True, - check=False, - ) - assert result.returncode == 0, ( - f"helm template failed:\n--- stdout ---\n{result.stdout[:2000]}\n--- stderr ---\n{result.stderr}" - ) - # Sanity: rendered output should include the Deployment. - assert "kind: Deployment" in result.stdout - assert "kind: Service" in result.stdout - - -def test_pyproject_has_server_extra(): - """The `server` extra should pin FastAPI + uvicorn for prod deployments.""" - text = (REPO_ROOT / "pyproject.toml").read_text() - assert "server = [" in text - # The block should mention FastAPI + uvicorn. - server_block_start = text.index("server = [") - server_block_end = text.index("]", server_block_start) - block = text[server_block_start:server_block_end] - assert "fastapi" in block - assert "uvicorn" in block