quickpat compose takes a composition spec — a YAML file declaring typed building blocks and custom application components — and generates a complete Validated Pattern directory. The same spec also produces a deployable QS Helm chart. One input, two outputs.
This tutorial walks through building the lemonade-stand composition spec and running it.
What you'll build: a VP for the lemonade-stand-assistant quickstart — a guardrailed chat assistant using TrustyAI orchestration. It has 3 model-serving instances, MinIO for detector model weights, custom microservices, and two frontends.
quickpat create analyzes an existing Helm chart and infers what the VP needs — operators, secrets, strategy. It works well for straightforward quickstarts and produces a VP matching the upstream chart's structure.
quickpat compose starts from a spec you author. Instead of inferring, you declare: these are the building blocks (model serving, object storage, GPU), this is the custom application code, these are the Vault secrets. The compiler translates that declaration into the VP. The spec is portable — it documents the application's full dependency graph in one readable file.
Use quickpat create when you have an existing quickstart and want a fast conversion. Use quickpat compose when you're authoring a new application, or when you want an explicit, reviewable spec that travels alongside the pattern.
A composition spec has four sections:
apiVersion: supplychain/v1alpha1
kind: ApplicationSpec
metadata: { ... } # application identity + upstream QS reference
blocks: { ... } # named infrastructure building blocks
wiring: [ ... ] # data-flow declarations between blocks
custom: { ... } # application-specific componentsmetadata:
name: lemonade-stand
description: LLM guardrails demo with TrustyAI orchestration
tier: sandbox # sandbox | tested | maintained
upstream:
repo: https://github.com/rh-ai-quickstart/lemonade-stand-assistant.git
path: chart
branch: mainname becomes the pattern name, default namespace, and Helm release name. upstream is the existing QS chart that the VP references as a remote ArgoCD application.
Each block is a named instance of a typed building block. The type encodes shared infrastructure knowledge — which operators to install, which namespaces to create, which local charts to add.
ai-platform-foundation — installs RHOAI, Serverless, and Service Mesh; creates the DataScienceCluster CR. Every AI application needs this.
blocks:
platform:
type: ai-platform-foundation
config:
dsc:
kserve: Managed
trustyai: Managed # needed for GuardrailsOrchestrator
dashboard: Managed
modelmeshserving: Managed
datasciencepipelines: Removed
kueue: Removed
ray: Removed
trainingoperator: Removed
workbenches: Removedgpu-compute — installs Node Feature Discovery and the NVIDIA GPU Operator; creates the ClusterPolicy.
gpu:
type: gpu-compute
config:
mig_strategy: single
dcgm: truemodel-serving — one block instance per model. The same block type handles chat LLMs (vLLM on GPU) and small classifiers (CPU). For lemonade-stand, there are three:
llm:
type: model-serving
profile: llm-inference # sets gpu: true, runtime: vllm defaults
config:
model: meta-llama/Llama-3.2-3B-Instruct
storage:
type: oci
uri: oci://quay.io/redhat-ai-services/modelcar-catalog:llama-3.2-3b-instruct
secrets:
vllm-api-key:
vault_path: lemonade-stand
hap-detector:
type: model-serving
config:
model: ibm-granite/granite-guardian-hap-125m
runtime: custom
image: quay.io/trustyai/guardrails-detector-huggingface-runtime:latest
gpu: false
storage:
type: s3
connection: "{{ blocks.model-storage.output.connection_name }}"
path: granite-guardian-hap-125m
prompt-injection-detector:
type: model-serving
config:
model: protectai/deberta-v3-base-prompt-injection-v2
runtime: custom
image: quay.io/trustyai/guardrails-detector-huggingface-runtime:latest
gpu: false
storage:
type: s3
connection: "{{ blocks.model-storage.output.connection_name }}"
path: deberta-v3-base-prompt-injection-v2The {{ blocks.model-storage.output.connection_name }} reference is a template variable — resolved at compile time from the object-storage block's declared outputs. The compiler validates that all references resolve.
object-storage — S3-compatible storage for model weights, datasets, or uploaded documents. The spec declares what the application needs (a named bucket); the deployment environment specifies which backend via platform overrides. Three providers are supported:
| Provider | When to use | What gets generated |
|---|---|---|
minio |
Bare metal / on-premises | PVC + Deployment + Service + bucket-init container |
odf |
Clusters with OpenShift Data Foundation | ObjectBucketClaim + setup Job |
s3 |
AWS, GCP, Azure (external bucket) | credentials Secret + data-connection only |
The data-connection Secret is always generated — it is the stable interface that all consumers (KServe model serving, ingestion pipelines, LlamaStack) reference regardless of backend.
model-storage:
type: object-storage
config:
provider: minio # minio | odf | s3 — can be overridden via platform values
bucket: huggingface
storage: 50Gi
init_models: # minio only: download these models at startup
- ibm-granite/granite-guardian-hap-125m
- protectai/deberta-v3-base-prompt-injection-v2
secrets:
access-key:
vault_path: lemonade-stand/minio
key: AWS_ACCESS_KEY_ID
secret-key:
vault_path: lemonade-stand/minio
key: AWS_SECRET_ACCESS_KEYTo deploy on AWS with S3 instead of MinIO, set in vp-out/overrides/values-AWS.yaml:
modelStorage:
provider: s3
endpoint: https://s3.amazonaws.com
region: us-east-1
bucket: my-lemonade-stand-bucket # pre-create this in your AWS accountNo spec change needed — the same spec produces a different backend on each platform.
guardrails-orchestrator — creates the TrustyAI GuardrailsOrchestrator CR and its routing ConfigMap.
guardrails:
type: guardrails-orchestrator
config:
enable_built_in_detectors: true
detectors:
hap:
endpoint: "{{ blocks.hap-detector.output.predictor_host }}"
port: 8000
prompt_injection:
endpoint: "{{ blocks.prompt-injection-detector.output.predictor_host }}"
port: 8000
llm:
endpoint: "{{ blocks.llm.output.predictor_host }}"Declarative data-flow between blocks. Used for validation (does the block graph make sense?) and documentation. Does not generate Kubernetes resources.
wiring:
- from: guardrails
to: llm
via: chat-generation
- from: model-storage
to: hap-detector
via: model-weights
- from: model-storage
to: prompt-injection-detector
via: model-weightsApplication-specific components that aren't reusable building blocks. These generate Deployment + Service resources, optionally with a Route.
custom:
lemonade-stand-app:
source:
image: quay.io/ckavili/lemon-fastapi:1.0.26
ports:
- { name: http, port: 8080, route: true }
env:
GUARDRAILS_ORCHESTRATOR_SERVICE_SERVICE_HOST: "{{ blocks.guardrails.output.service_host }}"
VLLM_MODEL: "{{ blocks.llm.config.model }}"
chunker-service:
source:
image: quay.io/rh-ee-mmisiura/chunkers:v2.0
ports:
- { name: grpc, port: 8085 }
lingua-detector:
source:
image: quay.io/ckavili/lingua-language-detector:0.0.25
ports:
- { name: http, port: 8080 }
shiny-dashboard:
source:
image: quay.io/sara_banderby/shinydashboard:fedora
ports:
- { name: http, port: 3838, route: true }
env:
METRICS_URL: "{{ custom.lemonade-stand-app.endpoint }}/metrics"The FastAPI app, chunker, language detector, and Shiny dashboard are unique to lemonade-stand — there's no reason to build block abstractions for them. The boundary rule: if a component appears in 3+ quickstarts, it should be a block; if it's unique to this application, it's custom.
For hand-written charts that need ArgoCD value overrides, declare extraValueFiles: [/overrides/<name>.yaml] and keep the values in <spec_dir>/overrides/<name>.yaml (copied into vp-out/overrides/ on every compose, same model as charts/). Or set extraValues: inline on the custom component (parity with upstream.extraValues).
Set deploy: manual on a component that's build-time-only and must not be ArgoCD-managed (e.g. a one-shot image-build pipeline run via make). Manual components are excluded from values-prod.yaml applications on the VP path and from the chart on the QS path, and neither generator emits a stub or copies a chart for them — there's nothing in the output for anything to reference.
A complete spec is in examples/lemonade-stand-compose.yaml. Run it:
quickpat compose examples/lemonade-stand-compose.yaml -o /tmp/lemonade-stand-patternOutput:
=== QuickPat Compose: ApplicationSpec -> Validated Pattern ===
Pattern generated: /tmp/lemonade-stand-pattern/
Files: 17
values-global.yaml
values-prod.yaml
Makefile
Makefile-common
pattern.sh
pattern-metadata.yaml
ansible.cfg
.ansible-lint
.gitignore
docs/quickstart-analysis.md
values-secret.yaml.template
charts/lemonade-stand-secrets/
overrides/values-AWS.yaml
overrides/values-Azure.yaml
overrides/values-GCP.yaml
overrides/values-IBMCloud.yaml
overrides/values-None.yaml
Blocks compiled: platform, gpu, llm, hap-detector, prompt-injection-detector, model-storage, guardrails
The heart of the VP. The compiler derived all of this from the 7 blocks in the spec:
clusterGroup:
name: prod
namespaces:
vault: {}
external-secrets-operator:
operatorGroup: true
targetNamespaces: []
external-secrets: {}
openshift-nfd: {}
nvidia-gpu-operator: {}
redhat-ods-operator:
operatorGroup: true
targetNamespaces: []
openshift-serverless:
operatorGroup: true
targetNamespaces: []
lemonade-stand:
operatorGroup: true
targetNamespaces: [lemonade-stand]
labels:
opendatahub.io/dashboard: 'true'
modelmesh-enabled: 'false'
subscriptions:
nfd:
name: nfd
namespace: openshift-nfd
nvidia:
name: gpu-operator-certified
namespace: nvidia-gpu-operator
source: certified-operators
rhoai:
name: rhods-operator
namespace: redhat-ods-operator
serverless:
name: serverless-operator
namespace: openshift-serverless
servicemesh:
name: servicemeshoperator
namespace: openshift-operators
openshift-external-secrets:
name: openshift-external-secrets-operator
namespace: external-secrets-operator
channel: stable-v1
applications:
vault:
name: vault
namespace: vault
chart: hashicorp-vault
chartVersion: 0.1.*
openshift-external-secrets:
name: openshift-external-secrets
namespace: external-secrets
chart: openshift-external-secrets
chartVersion: 0.0.*
nfd:
name: nfd
namespace: openshift-nfd
path: charts/nfd
nvidia-config:
name: nvidia-config
namespace: nvidia-gpu-operator
path: charts/nvidia-config
dsc:
name: dsc
namespace: redhat-ods-operator
path: charts/dsc
lemonade-stand:
name: lemonade-stand
namespace: lemonade-stand
repoURL: https://github.com/rh-ai-quickstart/lemonade-stand-assistant.git
path: chart
chartVersion: main
lemonade-stand-secrets:
name: lemonade-stand-secrets
namespace: lemonade-stand
path: charts/lemonade-stand-secretsWhat each block contributed:
| Block | Subscriptions | Namespaces | ArgoCD apps |
|---|---|---|---|
platform (ai-platform-foundation) |
rhoai, serverless, servicemesh | redhat-ods-operator, openshift-serverless | dsc |
gpu (gpu-compute) |
nfd, nvidia | openshift-nfd, nvidia-gpu-operator | nfd, nvidia-config |
llm, hap-detector, prompt-injection-detector (model-serving) |
— | lemonade-stand (with OAI labels) | lemonade-stand (upstream QS) |
model-storage (object-storage) |
— | — | (part of upstream chart) |
guardrails (guardrails-orchestrator) |
— | — | (part of upstream chart) |
| Framework | openshift-external-secrets | vault, external-secrets-operator, external-secrets | vault, openshift-external-secrets, lemonade-stand-secrets |
The 3 Vault secrets declared across the blocks:
version: '2.0'
secrets:
- name: llm
vaultPrefixes: [hub]
fields:
- name: vllm-api-key
onMissingValue: prompt
- name: model-storage
vaultPrefixes: [hub]
fields:
- name: access-key
onMissingValue: prompt
- name: secret-key
onMissingValue: promptThe upstream chart hardcodes THEACCESSKEY and THESECRETKEY in the MinIO deployment. The spec declares them as Vault-managed secrets — an improvement the spec catches that a manual review would need to catch manually.
Besides block-level secrets, a spec can declare a top-level secrets: list — one named Vault-secret group per entry, independent of any block. Each field may set onMissingValue (prompt / skip / generate, default prompt) or, instead, a literal default:
secrets:
- name: inference
vault_path: my-pattern/inference
fields:
- name: provider
value: gemini # literal default, no prompt
- name: model
value: gemini-2.5-flash
- name: api_key
path: ~/.gemini-api-key # read from a local file at secret-load timevalue:emits a fixed default ({name, value}, noonMissingValue) — use for a field that always starts with a known value (e.g. a default provider).path:emits a file source ({name, path}, noonMissingValue) — use for a field whose value lives in a local file rather than being typed in or generated (e.g. an SSH key or an API key file).- Setting both on one field is a validator warning;
pathwins. - On the Vault-free QS path, the same field renders as a literal default (
value:) or is read from the file atcreate-secrets.shtime (path:), so QS and VP output stay behaviorally consistent.
cd /tmp/lemonade-stand-pattern
git init && git add -A && git commit -m "Initial pattern"
# Copy and fill in secrets
cp values-secret.yaml.template ~/values-secret-lemonade-stand.yaml
# Edit the file — add real values for vllm-api-key, access-key, secret-key
oc login <cluster>
./pattern.sh make installquickpat compose spec.yaml # VP output → vp-out/ (default)
quickpat compose spec.yaml --format qs # QS Helm chart → qs-out/
quickpat compose spec.yaml --output /path/to/out # explicit output directory
quickpat compose spec.yaml --no-fix # skip auto-fix validation (VP only)
quickpat compose spec.yaml --no-create-service-account # skip RBAC for ODF setup Job--create-service-account / --no-create-service-account: controls whether the
compiler generates a ServiceAccount, Role, and RoleBinding for the ODF storage
setup Job. Default: true (works out of the box). Pass --no-create-service-account
in environments with strict RBAC policies — you must pre-create the SA manually
(the Job template includes a comment showing the exact oc create commands).
metadata:
name: my-app
tier: sandbox # sandbox | tested | maintained
# devices: deployment modes the app supports.
# When declared, generates values-gpu.yaml and values-cpu.yaml device overrides.
# GPU operators (NFD, NVIDIA) move OUT of values-prod.yaml INTO values-gpu.yaml.
# At deploy time: set global.device=gpu or global.device=cpu.
devices: [cpu, gpu]
upstream:
repo: https://github.com/rh-ai-quickstart/RAG.git
path: deploy/helm/rag
branch: main
# extraValues: application-specific values for the upstream chart.
# Written to overrides/<app-name>.yaml and passed via extraValueFiles
# in the ArgoCD app entry. Use for things outside the block model —
# pipeline config, sample data, model parameters.
extraValues:
llm-service:
secret:
enabled: false
configure-pipeline:
sampleFileUpload:
enabled: true
# ignoreDifferences: resources the upstream chart manages at runtime.
# Copied directly into the ArgoCD app entry's ignoreDifferences list.
ignoreDifferences:
- group: datasciencepipelinesapplications.opendatahub.io
kind: DataSciencePipelinesApplication
jsonPointers: [/spec]
- group: route.openshift.io
kind: Route
jsonPointers: [/spec/host]| Block type | Operators installed | VP DSC injection | QS templates generated |
|---|---|---|---|
ai-platform-foundation |
openshift-ai, serverless, servicemesh | configured from spec | prereqs in NOTES.txt |
gpu-compute |
nvidia-gpu, nfd (→ device override when devices declared) |
ClusterPolicy | prereqs in NOTES.txt |
model-serving |
— | — | ServingRuntime + InferenceService |
object-storage |
— | — | Provider-conditional (see below) |
guardrails-orchestrator |
— | trustyai: Managed (via ai-platform-foundation) |
GuardrailsOrchestrator CR + ConfigMap |
vector-store |
— | — | pgvector Deployment + Service + Secret |
data-pipeline |
openshift-pipelines | datasciencepipelines: Managed (via ai-platform-foundation) |
Tekton Pipeline + Task + RBAC + trigger |
llama-stack |
— | llamastackoperator: Managed (auto-injected) |
Deployment + Service |
sso-auth |
— | — | Keycloak CR + Realm + RBAC |
| File | minio | odf | s3 |
|---|---|---|---|
pvc.yaml |
✅ guarded | — | — |
deployment.yaml |
✅ MinIO + bucket-init container | — | — |
service.yaml |
✅ | — | — |
obc.yaml |
— | ✅ ObjectBucketClaim | — |
obc-setup.yaml |
— | ✅ Job + optional RBAC | — |
credentials.yaml |
✅ from .Values.secrets.* |
— | ✅ from .Values.secrets.* |
data-connection.yaml |
✅ endpoint from service name | ✅ written by setup Job | ✅ endpoint from .Values.<key>.endpoint |
The data-pipeline block is the only block type with explicit input dependencies. It must declare which block provides the vector store and which provides object storage:
ingest:
type: data-pipeline
config:
sources:
- name: docs
type: s3
schedule: manual # manual | hourly | daily
chunk_size: 512
inputs:
vector_store: db # block name that provides pgvector endpoint + secret
object_storage: store # block name that provides S3 endpoint + credentialsThe compiler resolves the input block references and wires the correct endpoints, database names, bucket names, and Secret references into the Tekton Pipeline params.
- Per-instance local chart generation for
model-servingin VP output — the upstream QS chart is the application source (Level 1). Level 2 would give each block its own ArgoCD app. - External Helm chart dependencies in QS output — the compiler generates inline templates rather than pulling from
ai-architecture-charts. Inline is self-contained and portable; external deps are a future optimization.