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.

LAYER 1 Cluster Provisioner Runs ONCE per cluster OpenShift GitOps (ArgoCD) Red Hat Build of Keycloak Shared Gitea Instance LiteMaaS (AI Gateway) Separate playbook — NOT in AgV cluster-provision.yml Run by the developer LAYER 2 Sandbox API Runs per ORDER Schedule cluster from pool Create cluster-admin SA token Inject vars into AgnosticD Does NOT create namespaces Does NOT create OCP users Does NOT install workloads LAYER 3 AgnosticD / Tenant Roles Runs per ORDER (AgV drives this) 1. ocp4_workload_tenant_keycloak_user 2. ocp4_workload_tenant_namespace 3. ocp4_workload_tenant_gitea 4. ocp4_workload_litellm_virtual_keys 5. ocp4_workload_gitops_bootstrap 6. ocp4_workload_showroom Configured via AgV common.yaml

Full layer-by-layer explanation →


What Sandbox API Does in This Pattern

  1. Selects a cluster from the pre-warmed pool that was provisioned by Layer 1
  2. Creates a temporary cluster-admin service account so AgnosticD can authenticate with full privileges
  3. Injects four variables into the AgnosticD run environment (listed in the variables table below)
  4. Marks the cluster as in-use so it is not double-allocated
  5. When the order is destroyed: revokes the SA token and returns the cluster to the pool

Sandbox API does not:

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 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_username
common_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_repo
showroom_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.

Next: Scheduler-Only — Roles →