Deploy the AAP self-service portal (Red Hat Developer Hub with the AAP plugin) via Helm chart into an RHDP environment. Runs playbooks/portal.yml, which creates the OAuth application, deploys the chart, and syncs the org list. TRIGGER when: the user wants to deploy the self-service portal, asks about RHDH or Developer Hub, or wants non-admin users to launch templates from a browser. SKIP: if the portal is already deployed and the user wants to use it, or if the user wants to install OpenShift...
Installs into .claude/skills of the current project.
Are you the author of Sales Demos Portal?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/ericcames-sales-demos-portal)
---
name: sales-demos-portal
description: "Deploy the AAP self-service portal (Red Hat Developer Hub with the AAP plugin) via Helm chart into an RHDP environment. Runs playbooks/portal.yml, which creates the OAuth application, deploys the chart, and syncs the org list. TRIGGER when: the user wants to deploy the self-service portal, asks about RHDH or Developer Hub, or wants non-admin users to launch templates from a browser. SKIP: if the portal is already deployed and the user wants to use it, or if the user wants to install OpenShift Virtualization — that is sales-demos-setup."
---
# sales-demos-portal
Deploy the AAP self-service portal into an RHDP environment. Takes about 11
minutes end to end.
This skill contains **no logic**. All the work is in
[`playbooks/portal.yml`](../../../playbooks/portal.yml). See `CLAUDE.md` →
*Skills and playbooks*.
## What it does
1. Creates a gateway OAuth application (delete + recreate — never PATCH
`client_secret`, the gateway hashes differently and gives `invalid_client`).
2. Creates the `aap-portal` namespace, a durable service token, and three
Secrets the chart expects.
3. Deploys `redhat-rhaap-portal` 2.1.0 via Helm.
4. Updates the OAuth redirect URI to the actual portal Route.
5. Syncs the org list from AAP so all organizations are visible.
6. Patches the portal ConfigMap: 1-minute sync interval, permissions disabled
so non-admin users can see and launch templates.
## Preflight Check
```bash
./utilities/preflight.sh "${ENV:-sandbox}" --k8s
# helm binary is available
command -v helm >/dev/null \
&& echo "✅ helm $(helm version --short 2>/dev/null)" \
|| echo "❌ helm not found — https://helm.sh/docs/intro/install/"
# Kubeconfig for Helm module
ENV=${ENV:-sandbox}
test -f ".kube/${ENV}.kubeconfig" \
&& echo "✅ kubeconfig exists for $ENV" \
|| bash utilities/make-kubeconfig.sh "$ENV"
```
If any check fails, stop and tell the user exactly which one and the fix shown
beside it. Do not attempt the run with a failing prerequisite.
```bash
python3 - <<'PY'
import os, ssl, json, urllib.request
ctx = ssl.create_default_context(); ctx.check_hostname = False; ctx.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(os.environ["OCP_URL"].rstrip("/") + "/apis",
headers={"Authorization": "Bearer " + os.environ["OCP_TOKEN"]})
groups = json.load(urllib.request.urlopen(req, context=ctx, timeout=20))["groups"]
print("✅ cluster reachable — %d API groups" % len(groups))
PY
```
## Collect inputs
Only one input, and it has a default. Ask the user only if it is ambiguous:
| Variable | Default | Meaning |
|---|---|---|
| `ENV` (inventory limit) | `sandbox` | Which environment to target — `sandbox` or `demo` |
## Run
```bash
mkdir -p ~/ansible-logs
export ANSIBLE_LOG_PATH=~/ansible-logs/portal-sandbox-$(date +%F-%H%M).log
./utilities/run-ansible.sh playbooks/portal.yml -i inventory --limit sandbox -e target_env=sandbox \
--vault-id sales.demos@~/secrets/.vault_pass_sales_demos
```
**Always set `ANSIBLE_LOG_PATH`** — this run takes about 11 minutes and the log
is the only evidence left if it fails. Logs live outside the repo, in
`~/ansible-logs/`. Tell the user the path so they can find it later.
**Never pipe the run through `tee`.** In a pipeline the exit status comes from
`tee`, not `ansible-playbook`, so a failed run reports success.
Tell the user this takes about 11 minutes and stream the output.
## Verify it in the EE before merging a change
See `/sales-demos-verify-ee` for why and how. The one command:
```bash
utilities/run-in-ee.sh playbooks/portal.yml \
-i inventory --limit sandbox -e target_env=sandbox \
--vault-id sales.demos@~/secrets/.vault_pass_sales_demos
```
## Verify on the cluster
**A green playbook run is not proof.** Ask the cluster using the
`openshift-sandbox` (or `openshift-demo`) MCP tools:
1. Check namespace exists: `pods_list_in_namespace` for `aap-portal`
2. Check deployment is ready: `resources_get` for Deployment
`rhaap-portal-redhat-developer-hub` in `aap-portal`
3. Check Route exists: `resources_list` for Routes in `aap-portal`
Then open the portal URL in a browser. It should show the Red Hat Developer Hub
login page. Log in with the AAP credentials — templates from the configured
organizations should be visible.
4. Confirm the two self-service entry points are in the catalog:
`Self-Service - Request Linux Server` and
`Self-Service - Request Windows Server`. Each should render its survey as a
request form (hypervisor, VM size tier, workload role, how many VMs). Allow
one sync interval — the catalog refreshes every minute.
## The portal cannot surface workflows, and that is why those two exist
The catalog syncs JOB templates only. There is no setting to change it: the
plugin has no workflow provider at all.
**Measured 2026-09-08, not inferred from the docs** (which are silent on the
question). `playbooks/portal.yml` writes
`catalog.providers.rhaap.production.sync.jobTemplates` because that is the only
key available, and grepping the deployed plugin bundle inside the running
`rhaap-portal` pod (chart 2.1.0) returns 200 occurrences of `jobTemplates` and
**zero** of `workflowJobTemplates`, `WorkflowJobTemplate` or
`AAPWorkflowJobTemplateProvider`. dc1.azure reached the same conclusion the same
way and records it in its own `playbooks/launch_workflow.yml`.
So the demo's headline entry points — `Linux Day 1 - 0 Workflow`,
`Windows Day 1 - 0 Workflow`, `Cluster Day 0`, `Windows Day 2 - 0 Break Fix` —
are invisible to the portal on their own. #242 closed that for the two
provisioning paths with thin launcher job templates that fire the workflow
(`playbooks/launch_workflow.yml`). If a future workflow needs to be portal-
reachable, it needs a launcher too; do not go looking for a config key.
## When it finishes
Report the playbook summary **and** the verification result above, then tell the
user the portal is live and accessible at the URL shown.
## If it fails
| Symptom | Cause | Fix |
|---|---|---|
| `401` / `Unauthorized` on the first task | RHDP bearer token expired | Refresh `openshift_api_token` in the vault, re-run `make-kubeconfig.sh` |
| `invalid_client` at `/o/token/` | OAuth client_secret was PATCHed instead of recreated | The playbook deletes and recreates; if this still happens, manually delete the `aap-selfservice-portal` application in the gateway |
| Helm deploy hangs or times out | Chart repo unreachable or cluster resources exhausted | Check `helm repo add openshift-charts https://charts.openshift.io/ && helm search repo redhat-rhaap-portal` |
| `rhaap-portal-app-config not found` | Helm deployment failed silently | Check the Helm release: `helm list -n aap-portal --kubeconfig .kube/<env>.kubeconfig` |
| Portal shows `UNRECOGNIZED` or no templates | Org sync not applied or still syncing | Wait 1 minute for the sync interval, or re-run the playbook |
| A workflow is missing from the catalog | Expected — the plugin syncs job templates only, and has no workflow provider | Not a fault to fix. Give the workflow a launcher job template, as #242 did for the two provisioning workflows |
| `Self-Service - Request …` is missing | `config.yml` has not run since #242, or it ran against the other environment | Re-run `config.yml` with the right `--limit`, then wait one sync interval |
| `Attempting to decrypt but no vault secrets found` | `--vault-id` missing from the command | Add `--vault-id sales.demos@~/secrets/.vault_pass_sales_demos` |
Never paste a live cluster hostname or token into a commit message, issue, or
PR. This repo is public — see `CLAUDE.md`.