Scheduler-Only Pattern Summit 2026
Complete developer reference — AgnosticV common.yaml, role walkthrough, and destroy behavior
How It Works — Provisioning Flow
Three phases: cluster provisioner runs once, Sandbox API picks a cluster per order, AgnosticD roles create per-tenant resources. Only Layer 3 is configured in your AgV. See GitOps Pattern guide for the architecture.
Full layer-by-layer explanation →
What Sandbox API Does in This Pattern
- Selects a cluster from the pre-warmed pool that was provisioned by Layer 1
- Creates a temporary cluster-admin service account so AgnosticD can authenticate with full privileges
- Injects four variables into the AgnosticD run environment (listed in the variables table below)
- Marks the cluster as in-use so it is not double-allocated
- When the order is destroyed: revokes the SA token and returns the cluster to the pool
Sandbox API does not:
- Create any namespaces for this tenant
- Create any OCP user accounts
- Install any workloads
- Configure Keycloak, Gitea, ArgoCD, or LiteMaaS (that was Layer 1)
Why use Scheduler-Only?
Use this when your lab needs multiple namespaces, Keycloak SSO, Gitea repos, and ArgoCD per tenant. Sandbox API stays minimal — your Ansible roles control the exact tenant topology.
How does Sandbox API pick a cluster? How do I get the API URL and token?
Your
Full explanation with variable reference →
cloud_selector tags are matched against cluster annotations in the Sandbox API pool database. All matching clusters with available capacity are eligible — one is picked at random. After selection, sandbox_openshift_api_url, sandbox_openshift_ingress_domain, sandbox_openshift_console_url, and cluster_admin_agnosticd_sa_token are injected automatically into every role.
Full explanation with variable reference →
Complete AgV common.yaml — Annotated
Production-ready common.yaml for the Summit 2026 / Scheduler-Only pattern. Copy it as a starting point and modify values for your catalog item.
File location
catalog/<your-catalog-item>/common.yaml — merged with account.yaml and size.yaml at order time.
---
# ============================================================
# AgnosticV — common.yaml
# Pattern: Summit 2026 / Scheduler-Only
# Catalog item: MCP with OpenShift (Sandbox)
# The Sandbox API schedules a cluster. All namespaces, users,
# and resources are created by Ansible roles — not by sandbox API.
# ============================================================
# ── 1. INCLUDES ──────────────────────────────────────────────
# Standard AgV includes — wired by AgnosticV automatically.
# litellm_metadata: captures requester_email + catalog_item_name
# for LiteMaaS key attribution and usage tracking.
#include /includes/agd-v2-mapping.yaml
#include /includes/sandbox-api.yaml
#include /includes/catalog-icon-openshift.yaml
#include /includes/terms-of-service.yaml
#include /includes/parameters/purpose.yaml
#include /includes/parameters/salesforce-id.yaml
#include /includes/parameters/litellm_metadata.yaml
#include /includes/secrets/litemaas-master_api.yaml
# ── 2. MANDATORY VARS ────────────────────────────────────────
# cloud_provider: none — no cloud account needed.
# We use pre-provisioned OCP clusters from the sandbox pool.
# config: namespace — AgnosticD namespace config:
# - Runs workloads on provision
# - Runs remove_workloads in listed order on destroy
# - Sets K8S_AUTH_* env vars for kubernetes.core modules
cloud_provider: none
config: namespace
# ── 3. COLLECTIONS ───────────────────────────────────────────
# Extra Ansible collections installed at deploy time.
# Point to specific branches to pin the behavior you tested.
requirements_content:
collections:
- name: https://github.com/agnosticd/namespaced_workloads.git
type: git
version: tenant-roles # keycloak_user, namespace, gitea roles (merged to main)
- name: https://github.com/agnosticd/core_workloads.git
type: git
version: gitops-bootstrap-userinfo # finalizer + remove_workload + userinfo (merged to main)
- name: https://github.com/rhpds/rhpds.litellm_virtual_keys.git
type: git
version: main
- name: https://github.com/agnosticd/showroom.git
type: git
version: v1.5.1
# ── 4. WORKLOAD ORDER ────────────────────────────────────────
# Roles run in this exact order during provisioning.
# Each role sets facts consumed by the roles that follow it.
workloads:
- agnosticd.namespaced_workloads.ocp4_workload_tenant_keycloak_user # 1. Create RHBK user
- agnosticd.namespaced_workloads.ocp4_workload_tenant_namespace # 2. Create OCP namespaces
- agnosticd.namespaced_workloads.ocp4_workload_tenant_gitea # 3. Per-user Gitea org + repo mirror
- rhpds.litellm_virtual_keys.ocp4_workload_litellm_virtual_keys # 4. LiteMaaS virtual key
- agnosticd.core_workloads.ocp4_workload_gitops_bootstrap # 5. ArgoCD app-of-apps
- agnosticd.showroom.ocp4_workload_showroom # 6. Showroom tab UI
# NOTE: ocp4_workload_ocp_console_embed is in the cluster provisioner (runs once).
# Do NOT add it here — it triggers a router rollout on every order.
# ── 5. DESTROY ORDER ─────────────────────────────────────────
# Explicit destroy order — runs in the ORDER listed (not reversed).
# External resources (LiteMaaS key, ArgoCD app) must be removed
# before namespace deletion. ArgoCD cascade handles workload cleanup.
remove_workloads:
- rhpds.litellm_virtual_keys.ocp4_workload_litellm_virtual_keys
- agnosticd.core_workloads.ocp4_workload_gitops_bootstrap # cascade deletes all ArgoCD apps first
- agnosticd.namespaced_workloads.ocp4_workload_tenant_gitea
- agnosticd.namespaced_workloads.ocp4_workload_tenant_namespace
- agnosticd.namespaced_workloads.ocp4_workload_tenant_keycloak_user
# ── 6. IDENTITY: USERNAME AND PASSWORD ───────────────────────
# ocp4_workload_tenant_keycloak_username is the single identity var.
# ALL tenant roles reference this one variable.
# Format: mcpuser-<guid> e.g. mcpuser-drw4x
ocp4_workload_tenant_keycloak_username: "mcpuser-{{ guid }}"
ocp4_workload_tenant_namespace_username: "{{ ocp4_workload_tenant_keycloak_username }}"
# Deterministic password — same value every time for the same GUID.
# Satisfies complexity: uppercase + lowercase + digit + special char.
common_password: "Mcp{{ (guid | hash('sha256'))[:8] }}!"
ocp4_workload_tenant_keycloak_user_password: "{{ common_password }}"
# ── 7. NAMESPACES ────────────────────────────────────────────
# ocp4_workload_tenant_namespace creates one namespace per suffix.
# Format: {suffix}-{username} e.g. librechat-mcpuser-drw4x
# All namespaces are pre-created by Ansible before ArgoCD syncs.
ocp4_workload_tenant_namespace_prefix: "{{ ocp4_workload_tenant_keycloak_username }}"
ocp4_workload_tenant_namespace_suffixes:
- agent
- librechat
- mcp-gitea
- mcp-openshift
- gitea
- showroom
ocp4_workload_tenant_namespace_limit_range:
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 50m
memory: 128Mi
# ── 8. GITEA ─────────────────────────────────────────────────
# Shared Gitea instance on the cluster (installed by cluster provisioner).
# This role creates one org + repo per user and mirrors the GitOps repo.
# ArgoCD reads from this mirrored repo — not directly from GitHub.
ocp4_workload_tenant_gitea_namespace: "gitea-{{ ocp4_workload_tenant_keycloak_username }}"
ocp4_workload_tenant_gitea_username: "{{ ocp4_workload_tenant_keycloak_username }}"
ocp4_workload_tenant_gitea_password: "{{ common_password }}"
ocp4_workload_tenant_gitea_admin_password: "{{ common_password }}"
ocp4_workload_tenant_gitea_repositories:
- name: mcp
repo: https://github.com/rhpds/ocpsandbox-mcp-with-openshift-gitops
private: false
# ── 9. LITEMAAS VIRTUAL KEY ──────────────────────────────────
# Creates a rate-limited AI API key scoped to this tenant.
# The role exports: litellm_virtual_key, litellm_api_endpoint,
# litellm_available_models — consumed by gitops_bootstrap below.
ocp4_workload_litellm_virtual_keys_duration: "7d"
ocp4_workload_litellm_virtual_keys_models:
- qwen3-14b
- llama-scout-17b
ocp4_workload_litellm_virtual_keys_multi_user: false
ocp4_workload_litellm_virtual_keys_user_count: 1
ocp4_workload_litellm_virtual_keys_verify_ssl: true
# ── 10. GITOPS BOOTSTRAP ─────────────────────────────────────
# Creates one ArgoCD Application: bootstrap-tenant
# That app-of-apps deploys all per-user workloads via ArgoCD.
# repo_url is set automatically by the tenant_gitea role.
ocp4_workload_gitops_bootstrap_application_name: "bootstrap-tenant"
ocp4_workload_gitops_bootstrap_repo_path: "tenant/bootstrap"
ocp4_workload_gitops_bootstrap_repo_revision: main
ocp4_workload_gitops_bootstrap_helm_values:
tenant:
username: "{{ ocp4_workload_tenant_keycloak_username }}"
password: "{{ common_password }}"
litemaas:
url: "{{ litellm_api_endpoint | default('') }}/v1"
key: "{{ litellm_virtual_key | default('') }}"
models: "{{ litellm_available_models | default([]) | join(',') }}"
# ── 11. SHOWROOM ─────────────────────────────────────────────
# Showroom is the tab-based UI the lab attendee sees in their browser.
ocp4_workload_showroom_namespace: "showroom-{{ ocp4_workload_tenant_keycloak_username }}"
ocp4_workload_showroom_content_git_repo: https://github.com/rhpds/lb1726-mcp-showroom
ocp4_workload_showroom_content_git_repo_ref: sandbox-login-instructions
# Set terminal_type: "" if your lab does not need an embedded OCP terminal.
# Default is "showroom" which adds a terminal tab requiring zero-touch-config.yml.
ocp4_workload_showroom_terminal_type: ""
#
# ── SHOWROOM TAB URLs ─────────────────────────────────────────
# In your showroom content repo ui-config.yml, use user_data variables
# directly — do NOT use ${USER}, it is always empty in single-user mode.
#
# tabs:
# - name: OpenShift Console
# url: '${OPENSHIFT_CONSOLE_URL}'
# - name: LibreChat
# url: '${LIBRECHAT_URL}'
# - name: Gitea
# url: '${GITEA_URL}'
#
# NOTE: ${LIBRECHAT_URL} and other GitOps-sourced URLs require
# ocp4_workload_gitops_bootstrap to read the userinfo ConfigMap and
# export it via agnosticd_user_info. This is available once
# Judd's judd-roundtrip PR merges into core_workloads main.
# See: https://github.com/agnosticd/core_workloads/tree/judd-roundtrip
# ── COMPATIBILITY MAPPINGS ────────────────────────────────────
# Sandbox API provides variables with the sandbox_ prefix.
# Older roles (showroom, ocp_console_embed, etc.) were written for
# config: openshift-workloads and expect the names below.
# These three lines bridge the gap — required in every catalog item.
#
# openshift_cluster_ingress_domain: showroom uses this for Route hostnames.
# Without it, Showroom gets an empty domain and Routes have no hostname.
openshift_cluster_ingress_domain: "{{ sandbox_openshift_ingress_domain }}"
openshift_api_url: "{{ sandbox_openshift_api_url }}"
openshift_cluster_admin_token: "{{ cluster_admin_agnosticd_sa_token }}"
# ── 12. METADATA (__meta__) ──────────────────────────────────
# Babylon/AgnosticV metadata: catalog display, ownership, deployer,
# and the sandbox API request (cluster selector + quota).
# The sandboxes entry lives here — it tells the Sandbox API
# which cluster to schedule and what quota to apply.
__meta__:
asset_uuid: 8030d69b-c145-48fa-a8a9-219ffa0f780e
owners:
maintainer:
- name: Wolfgang Kulhanek
email: wkulhane@redhat.com
- name: Tony Kay
email: ankay@redhat.com
deployer:
scm_url: https://github.com/agnosticd/agnosticd-v2
scm_ref: main
execution_environment:
image: quay.io/agnosticd/ee-multicloud:chained-2025-12-17
pull: missing
catalog:
namespace: "babylon-catalog-{{ stage | default('?') }}"
display_name: "MCP with OpenShift (Sandbox)"
category: Workshops
keywords:
- mcp
- openshift
- ai
labels:
Product: Red_Hat_OpenShift_Container_Platform
Provider: RHDP
multiuser: false
workshopLabUiRedirect: true
reportingLabels:
primaryBU: Hybrid_Platforms
secondaryBU: Artificial_Intelligence
sandbox_api:
actions:
destroy:
catch_all: false # REQUIRED — prevents deleting other tenants' LiteMaaS keys on destroy
# ── SANDBOX ENTRY ──────────────────────────────────────────
# ALL THREE cloud_selector tags are required.
# Sandbox API matches clusters whose annotations contain ALL your tags.
# Missing even one tag = no cluster found = order fails immediately.
sandboxes:
- kind: OcpSandbox
alias: cluster
namespace_suffix: user
cloud_selector:
cloud: cnv-dedicated-shared # required
demo: mcp-with-openshift # required — key and value set by admin when cluster registered
# ask admin: what tags does my cluster have?
purpose: prod # required
quota:
limits.cpu: "2"
requests.cpu: "2"
limits.memory: 4Gi
requests.memory: 4Gi
requests.storage: 10Gi
limit_range:
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 50m
memory: 128Mi
Section-by-section breakdown
| Section | Key variable(s) | What it does / Why it matters |
|---|---|---|
| 1. sandboxes | kind: OcpSandbox, alias: cluster |
Single entry, no namespace_suffix. Tells Sandbox API this is a scheduler-only request — just give me a cluster. No namespaces are created by Sandbox API. |
| 2. Username & password | ocp4_workload_tenant_keycloak_usernamecommon_password |
Define once at the top. All roles reference these two vars. The password formula is deterministic — the same GUID always produces the same password, which is useful for retries. Satisfies typical password complexity requirements. |
| 3. workloads | workloads: |
Ordered list of AgnosticD roles. AgnosticD calls each role's workload.yml in sequence. Role output facts are available to subsequent roles. Order matters — Keycloak user must exist before namespace creation, Gitea repo must exist before GitOps bootstrap. |
| 4. Keycloak config | ocp4_workload_tenant_keycloak_user_* |
Configures the RHBK host and realm. RHBK was installed by the cluster provisioner. The role creates one user per order. The admin password here is for the Keycloak admin API, not the end user. |
| 5. Namespace suffixes | ocp4_workload_tenant_namespace_suffixes |
List of suffix strings. The role creates one namespace per suffix, prefixed with the username. Example: mcpuser-drw4x-gitea. Each namespace gets a LimitRange automatically. |
| 6. Gitea config | ocp4_workload_tenant_gitea_* |
Configures the shared Gitea host. The role creates a Gitea organization named after the tenant user, then mirrors the specified source repos into that org. ArgoCD watches this tenant-specific Gitea org. |
| 7. LiteMaaS virtual key | ocp4_workload_litellm_virtual_keys_* |
Creates a rate-limited AI API key. The admin key comes from an env var — never hardcode it. catch_all: false is mandatory on shared clusters. After the role runs, litellm_virtual_key is set as a fact for downstream roles. |
| 8. GitOps bootstrap | ocp4_workload_gitops_bootstrap_* |
Creates an ArgoCD Application (app-of-apps pattern) that deploys all tenant workloads. The repo URL is automatically set by the Gitea role. Helm values are passed to the bootstrap chart, which propagates them to all child apps. |
| 9. Showroom | showroom_git_reposhowroom_user_data |
Deploys the Showroom tab UI. showroom_user_data is a dict of per-tenant values that Showroom renders into its tab configuration. Must be the last workload deployed so all URLs are ready. |
| 10. remove_workloads | remove_workloads: |
Exact reverse of workloads. Showroom goes first (UI down), then LiteMaaS key revoked, then ArgoCD app deleted (cascade deletes all tenant Kubernetes resources), then Gitea org, then namespaces, then Keycloak user. |