From b1f1ed71d2770fb524de96348abfce96846b264f Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 5 Aug 2026 00:53:06 +0200 Subject: [PATCH 1/5] fix(docs): correct Jenkins external-CI example payload and token request Signed-off-by: Kavith Lokuhewage --- .../workflows/external-ci.mdx | 42 ++++++++++++++++--- .../workflows/external-ci.mdx | 42 ++++++++++++++++--- 2 files changed, 72 insertions(+), 12 deletions(-) diff --git a/docs/platform-engineer-guide/workflows/external-ci.mdx b/docs/platform-engineer-guide/workflows/external-ci.mdx index be14c417..20f77a95 100644 --- a/docs/platform-engineer-guide/workflows/external-ci.mdx +++ b/docs/platform-engineer-guide/workflows/external-ci.mdx @@ -69,10 +69,15 @@ You can test the credentials by exchanging them for an access token: ```bash curl -X POST "/oauth2/token" \ - -u ":" \ - -d "grant_type=client_credentials" + -d "grant_type=client_credentials" \ + -d "client_id=" \ + -d "client_secret=" ``` +:::note +Backend Service applications are registered with `token_endpoint_auth_method: client_secret_post`, so the client credentials must be sent as form parameters in the request body. Passing them as HTTP Basic credentials (`curl -u`) is rejected with `unauthorized_client`. +::: + :::tip For long-running CI pipelines, configure a longer token validity period in the application settings within the ThunderID console. ::: @@ -139,19 +144,44 @@ pipeline { string(credentialsId: 'openchoreo-api-url', variable: 'OPENCHOREO_API_URL') ]) { sh ''' - # Get access token + set -eu + + # 1. Get an access token (client_credentials grant) TOKEN=$(curl -sf -X POST "${THUNDER_URL}/oauth2/token" \ -d "grant_type=client_credentials" \ -d "client_id=${CLIENT_ID}" \ -d "client_secret=${CLIENT_SECRET}" \ | jq -r '.access_token') - # Create/update workload - curl -sf -X POST \ + # 2. Build the Workload CR payload + WORKLOAD_NAME="${COMPONENT}-workload" + cat > workload-cr.json </oauth2/token" \ - -u ":" \ - -d "grant_type=client_credentials" + -d "grant_type=client_credentials" \ + -d "client_id=" \ + -d "client_secret=" ``` +:::note +Backend Service applications are registered with `token_endpoint_auth_method: client_secret_post`, so the client credentials must be sent as form parameters in the request body. Passing them as HTTP Basic credentials (`curl -u`) is rejected with `unauthorized_client`. +::: + :::tip For long-running CI pipelines, configure a longer token validity period in the application settings within the ThunderID console. ::: @@ -139,19 +144,44 @@ pipeline { string(credentialsId: 'openchoreo-api-url', variable: 'OPENCHOREO_API_URL') ]) { sh ''' - # Get access token + set -eu + + # 1. Get an access token (client_credentials grant) TOKEN=$(curl -sf -X POST "${THUNDER_URL}/oauth2/token" \ -d "grant_type=client_credentials" \ -d "client_id=${CLIENT_ID}" \ -d "client_secret=${CLIENT_SECRET}" \ | jq -r '.access_token') - # Create/update workload - curl -sf -X POST \ + # 2. Build the Workload CR payload + WORKLOAD_NAME="${COMPONENT}-workload" + cat > workload-cr.json < Date: Wed, 5 Aug 2026 04:43:23 +0200 Subject: [PATCH 2/5] docs: address review feedback on external-CI examples - url-encode OAuth form values - check the PUT fallback status, not just curl -sf - note the jq prerequisite and that jenkins/jenkins:lts lacks it - set +x so the bearer token is not echoed into the build log Signed-off-by: Kavith Lokuhewage --- .../workflows/external-ci.mdx | 52 +++++++++++++------ .../workflows/external-ci.mdx | 52 +++++++++++++------ 2 files changed, 74 insertions(+), 30 deletions(-) diff --git a/docs/platform-engineer-guide/workflows/external-ci.mdx b/docs/platform-engineer-guide/workflows/external-ci.mdx index 20f77a95..1c06866d 100644 --- a/docs/platform-engineer-guide/workflows/external-ci.mdx +++ b/docs/platform-engineer-guide/workflows/external-ci.mdx @@ -43,6 +43,19 @@ Steps 1–3 cover wiring an external CI pipeline to the Workload API, with worke - Access to your identity provider (ThunderID IDP or configured OIDC provider) - Your CI system (Jenkins, GitHub Actions, or any other) configured and accessible - Container registry accessible to both CI and OpenChoreo cluster +- `curl` and `jq` available on the machine running the pipeline + +:::note +The examples below use `jq` to read the access token out of the token response. GitHub Actions runners include it, but the official `jenkins/jenkins:lts` image does **not** — a pipeline on a stock Jenkins agent fails with `jq: not found`. Install it on the agent, or bake it into your image: + +```dockerfile +FROM jenkins/jenkins:lts +USER root +RUN apt-get update && apt-get install -y --no-install-recommends jq \ + && rm -rf /var/lib/apt/lists/* +USER jenkins +``` +::: ## Step 1: Create a Service Account @@ -69,13 +82,15 @@ You can test the credentials by exchanging them for an access token: ```bash curl -X POST "/oauth2/token" \ - -d "grant_type=client_credentials" \ - -d "client_id=" \ - -d "client_secret=" + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=" \ + --data-urlencode "client_secret=" ``` :::note Backend Service applications are registered with `token_endpoint_auth_method: client_secret_post`, so the client credentials must be sent as form parameters in the request body. Passing them as HTTP Basic credentials (`curl -u`) is rejected with `unauthorized_client`. + +Use `--data-urlencode` rather than `-d`: a client secret containing `&`, `+`, or `=` would otherwise be split into extra form fields and the request would fail. ::: :::tip @@ -146,11 +161,16 @@ pipeline { sh ''' set -eu + # Jenkins runs sh with -x. Jenkins masks the credentials it + # injected, but not the token derived from them -- without + # this, the bearer token is echoed into the build log. + set +x + # 1. Get an access token (client_credentials grant) TOKEN=$(curl -sf -X POST "${THUNDER_URL}/oauth2/token" \ - -d "grant_type=client_credentials" \ - -d "client_id=${CLIENT_ID}" \ - -d "client_secret=${CLIENT_SECRET}" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=${CLIENT_ID}" \ + --data-urlencode "client_secret=${CLIENT_SECRET}" \ | jq -r '.access_token') # 2. Build the Workload CR payload @@ -174,12 +194,14 @@ EOF -d @workload-cr.json) if [ "${CODE}" = "409" ]; then - curl -sf -X PUT \ + CODE=$(curl -s -o resp.json -w '%{http_code}' -X PUT \ "${OPENCHOREO_API_URL}/api/v1/namespaces/${NAMESPACE}/workloads/${WORKLOAD_NAME}" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ - -d @workload-cr.json - elif [ "${CODE}" -lt 200 ] || [ "${CODE}" -ge 300 ]; then + -d @workload-cr.json) + fi + + if [ "${CODE}" -lt 200 ] || [ "${CODE}" -ge 300 ]; then echo "Workload registration failed (HTTP ${CODE}):"; cat resp.json; exit 1 fi ''' @@ -238,9 +260,9 @@ jobs: # 1. Get an access token (client_credentials grant) TOKEN=$(curl -sf -X POST "${THUNDER_URL}/oauth2/token" \ - -d "grant_type=client_credentials" \ - -d "client_id=${CLIENT_ID}" \ - -d "client_secret=${CLIENT_SECRET}" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=${CLIENT_ID}" \ + --data-urlencode "client_secret=${CLIENT_SECRET}" \ | jq -r '.access_token') # 2. Build the Workload CR payload (image only; add endpoints and @@ -606,9 +628,9 @@ When configured correctly: ```bash curl -v -X POST "$THUNDER_URL/oauth2/token" \ - -d "grant_type=client_credentials" \ - -d "client_id=$CLIENT_ID" \ - -d "client_secret=$CLIENT_SECRET" + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=$CLIENT_ID" \ + --data-urlencode "client_secret=$CLIENT_SECRET" ``` 2. **Test API connectivity** with a simple GET: diff --git a/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx b/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx index 20f77a95..1c06866d 100644 --- a/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx +++ b/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx @@ -43,6 +43,19 @@ Steps 1–3 cover wiring an external CI pipeline to the Workload API, with worke - Access to your identity provider (ThunderID IDP or configured OIDC provider) - Your CI system (Jenkins, GitHub Actions, or any other) configured and accessible - Container registry accessible to both CI and OpenChoreo cluster +- `curl` and `jq` available on the machine running the pipeline + +:::note +The examples below use `jq` to read the access token out of the token response. GitHub Actions runners include it, but the official `jenkins/jenkins:lts` image does **not** — a pipeline on a stock Jenkins agent fails with `jq: not found`. Install it on the agent, or bake it into your image: + +```dockerfile +FROM jenkins/jenkins:lts +USER root +RUN apt-get update && apt-get install -y --no-install-recommends jq \ + && rm -rf /var/lib/apt/lists/* +USER jenkins +``` +::: ## Step 1: Create a Service Account @@ -69,13 +82,15 @@ You can test the credentials by exchanging them for an access token: ```bash curl -X POST "/oauth2/token" \ - -d "grant_type=client_credentials" \ - -d "client_id=" \ - -d "client_secret=" + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=" \ + --data-urlencode "client_secret=" ``` :::note Backend Service applications are registered with `token_endpoint_auth_method: client_secret_post`, so the client credentials must be sent as form parameters in the request body. Passing them as HTTP Basic credentials (`curl -u`) is rejected with `unauthorized_client`. + +Use `--data-urlencode` rather than `-d`: a client secret containing `&`, `+`, or `=` would otherwise be split into extra form fields and the request would fail. ::: :::tip @@ -146,11 +161,16 @@ pipeline { sh ''' set -eu + # Jenkins runs sh with -x. Jenkins masks the credentials it + # injected, but not the token derived from them -- without + # this, the bearer token is echoed into the build log. + set +x + # 1. Get an access token (client_credentials grant) TOKEN=$(curl -sf -X POST "${THUNDER_URL}/oauth2/token" \ - -d "grant_type=client_credentials" \ - -d "client_id=${CLIENT_ID}" \ - -d "client_secret=${CLIENT_SECRET}" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=${CLIENT_ID}" \ + --data-urlencode "client_secret=${CLIENT_SECRET}" \ | jq -r '.access_token') # 2. Build the Workload CR payload @@ -174,12 +194,14 @@ EOF -d @workload-cr.json) if [ "${CODE}" = "409" ]; then - curl -sf -X PUT \ + CODE=$(curl -s -o resp.json -w '%{http_code}' -X PUT \ "${OPENCHOREO_API_URL}/api/v1/namespaces/${NAMESPACE}/workloads/${WORKLOAD_NAME}" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ - -d @workload-cr.json - elif [ "${CODE}" -lt 200 ] || [ "${CODE}" -ge 300 ]; then + -d @workload-cr.json) + fi + + if [ "${CODE}" -lt 200 ] || [ "${CODE}" -ge 300 ]; then echo "Workload registration failed (HTTP ${CODE}):"; cat resp.json; exit 1 fi ''' @@ -238,9 +260,9 @@ jobs: # 1. Get an access token (client_credentials grant) TOKEN=$(curl -sf -X POST "${THUNDER_URL}/oauth2/token" \ - -d "grant_type=client_credentials" \ - -d "client_id=${CLIENT_ID}" \ - -d "client_secret=${CLIENT_SECRET}" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=${CLIENT_ID}" \ + --data-urlencode "client_secret=${CLIENT_SECRET}" \ | jq -r '.access_token') # 2. Build the Workload CR payload (image only; add endpoints and @@ -606,9 +628,9 @@ When configured correctly: ```bash curl -v -X POST "$THUNDER_URL/oauth2/token" \ - -d "grant_type=client_credentials" \ - -d "client_id=$CLIENT_ID" \ - -d "client_secret=$CLIENT_SECRET" + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=$CLIENT_ID" \ + --data-urlencode "client_secret=$CLIENT_SECRET" ``` 2. **Test API connectivity** with a simple GET: From fffbd8d049ba47aea54e26d9212e9a426925697f Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 5 Aug 2026 06:31:32 +0200 Subject: [PATCH 3/5] docs: correct the jenkins.io/job-full-name annotation value The documented /job/my-org/job/my-service is a URL path; the plugin expects a job full name and inserts the /job/ segments itself. Signed-off-by: Kavith Lokuhewage --- docs/developer-guide/workflows/ci/overview.md | 2 +- .../platform-engineer-guide/workflows/external-ci.mdx | 11 ++++++++++- .../developer-guide/workflows/ci/overview.md | 2 +- .../platform-engineer-guide/workflows/external-ci.mdx | 11 ++++++++++- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/developer-guide/workflows/ci/overview.md b/docs/developer-guide/workflows/ci/overview.md index 951e27b6..c62a535a 100644 --- a/docs/developer-guide/workflows/ci/overview.md +++ b/docs/developer-guide/workflows/ci/overview.md @@ -187,7 +187,7 @@ If your organization uses an external CI platform (e.g., Jenkins, GitHub Actions 1. Navigate to **Create** in Backstage 2. Select your component type and fill in **Component Metadata** 3. In the **Build & Deploy** step, under **Deployment Source**, select **"External CI"** -4. Optionally select your **CI Platform** (e.g., Jenkins) to enable build visibility in Backstage, and provide the **Jenkins Job Path** (e.g., `/job/my-org/job/my-service`) +4. Optionally select your **CI Platform** (e.g., Jenkins) to enable build visibility in Backstage, and provide the **Jenkins job full name** (e.g., `my-org/my-service`, or just `my-service` for a job outside a folder — not the `/job/...` URL path) 5. Complete the remaining steps and review The component is created without a workload. Your CI pipeline will create workloads when builds complete by calling the OpenChoreo Workload API: diff --git a/docs/platform-engineer-guide/workflows/external-ci.mdx b/docs/platform-engineer-guide/workflows/external-ci.mdx index 1c06866d..7d8d56e3 100644 --- a/docs/platform-engineer-guide/workflows/external-ci.mdx +++ b/docs/platform-engineer-guide/workflows/external-ci.mdx @@ -397,9 +397,18 @@ Once the plugin is enabled, add the Jenkins annotation to your components to dis 1. Navigate to your component in Backstage 2. Click the context menu (**...**) and select **Edit Annotations** -3. Add the annotation: `jenkins.io/job-full-name` = `/job/my-org/job/my-service` +3. Add the annotation: `jenkins.io/job-full-name` = `my-org/my-service` 4. Click **Save** +:::note +The value is the job's **full name** — folder segments separated by `/` — not the +Jenkins URL path. Use `my-org/my-service`, or just `my-service` for a job that is not +inside a folder. The plugin inserts the `/job/` segments itself, so a URL-style value +like `/job/my-org/job/my-service` is read as a folder literally named `job` and +resolves to `/job/job/job/my-org/...`, which 404s. The card then spins indefinitely +and the Jenkins tab shows no builds. +::: + ### What You'll See When configured correctly: diff --git a/versioned_docs/version-v1.2.x/developer-guide/workflows/ci/overview.md b/versioned_docs/version-v1.2.x/developer-guide/workflows/ci/overview.md index 951e27b6..c62a535a 100644 --- a/versioned_docs/version-v1.2.x/developer-guide/workflows/ci/overview.md +++ b/versioned_docs/version-v1.2.x/developer-guide/workflows/ci/overview.md @@ -187,7 +187,7 @@ If your organization uses an external CI platform (e.g., Jenkins, GitHub Actions 1. Navigate to **Create** in Backstage 2. Select your component type and fill in **Component Metadata** 3. In the **Build & Deploy** step, under **Deployment Source**, select **"External CI"** -4. Optionally select your **CI Platform** (e.g., Jenkins) to enable build visibility in Backstage, and provide the **Jenkins Job Path** (e.g., `/job/my-org/job/my-service`) +4. Optionally select your **CI Platform** (e.g., Jenkins) to enable build visibility in Backstage, and provide the **Jenkins job full name** (e.g., `my-org/my-service`, or just `my-service` for a job outside a folder — not the `/job/...` URL path) 5. Complete the remaining steps and review The component is created without a workload. Your CI pipeline will create workloads when builds complete by calling the OpenChoreo Workload API: diff --git a/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx b/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx index 1c06866d..7d8d56e3 100644 --- a/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx +++ b/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx @@ -397,9 +397,18 @@ Once the plugin is enabled, add the Jenkins annotation to your components to dis 1. Navigate to your component in Backstage 2. Click the context menu (**...**) and select **Edit Annotations** -3. Add the annotation: `jenkins.io/job-full-name` = `/job/my-org/job/my-service` +3. Add the annotation: `jenkins.io/job-full-name` = `my-org/my-service` 4. Click **Save** +:::note +The value is the job's **full name** — folder segments separated by `/` — not the +Jenkins URL path. Use `my-org/my-service`, or just `my-service` for a job that is not +inside a folder. The plugin inserts the `/job/` segments itself, so a URL-style value +like `/job/my-org/job/my-service` is read as a folder literally named `job` and +resolves to `/job/job/job/my-org/...`, which 404s. The card then spins indefinitely +and the Jenkins tab shows no builds. +::: + ### What You'll See When configured correctly: From a0d2cd16b044286930c9ecbcc1c16d023aa486b9 Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 5 Aug 2026 06:53:58 +0200 Subject: [PATCH 4/5] docs: fix prettier formatting in external-ci guide Signed-off-by: Kavith Lokuhewage --- docs/platform-engineer-guide/workflows/external-ci.mdx | 1 + .../platform-engineer-guide/workflows/external-ci.mdx | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/platform-engineer-guide/workflows/external-ci.mdx b/docs/platform-engineer-guide/workflows/external-ci.mdx index 7d8d56e3..0ac0f884 100644 --- a/docs/platform-engineer-guide/workflows/external-ci.mdx +++ b/docs/platform-engineer-guide/workflows/external-ci.mdx @@ -55,6 +55,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends jq \ && rm -rf /var/lib/apt/lists/* USER jenkins ``` + ::: ## Step 1: Create a Service Account diff --git a/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx b/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx index 7d8d56e3..0ac0f884 100644 --- a/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx +++ b/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx @@ -55,6 +55,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends jq \ && rm -rf /var/lib/apt/lists/* USER jenkins ``` + ::: ## Step 1: Create a Service Account From 41a9e524a7fe1a4620a930210e9c9d594e684b1c Mon Sep 17 00:00:00 2001 From: Kavith Lokuhewage Date: Wed, 5 Aug 2026 09:17:01 +0200 Subject: [PATCH 5/5] docs: add occ CLI alternative to the external CI guide Signed-off-by: Kavith Lokuhewage --- .../workflows/external-ci.mdx | 49 +++++++++++++++++++ .../workflows/external-ci.mdx | 49 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/docs/platform-engineer-guide/workflows/external-ci.mdx b/docs/platform-engineer-guide/workflows/external-ci.mdx index 0ac0f884..a3069331 100644 --- a/docs/platform-engineer-guide/workflows/external-ci.mdx +++ b/docs/platform-engineer-guide/workflows/external-ci.mdx @@ -213,6 +213,55 @@ EOF } ``` +#### Using the OpenChoreo CLI instead of curl + +If you would rather not manage raw `curl` calls, the [`occ` CLI](../cli-configuration.mdx) performs the same registration. `occ apply` checks whether the workload exists and creates or updates it, so the 409 fallback disappears, and API errors surface as readable messages with a non-zero exit code — no `jq` needed. The CLI reads its credentials from environment variables and never prints the access token, so the `set +x` guard is also unnecessary. + +Reusing the same Jenkins credentials, the deploy stage becomes: + +```groovy +stage('Deploy to OpenChoreo') { + steps { + withCredentials([ + usernamePassword( + credentialsId: 'workflows-credentials', + usernameVariable: 'OCC_CLIENT_ID', + passwordVariable: 'OCC_CLIENT_SECRET' + ), + string(credentialsId: 'openchoreo-api-url', variable: 'OPENCHOREO_API_URL') + ]) { + sh ''' + set -eu + + # occ stores its login (including the client secret) in + # ${HOME}/.openchoreo/config -- keep it inside the per-build + # workspace on shared agents. + export HOME="${WORKSPACE}" + + # Pin the CLI to the tag matching your OpenChoreo release. + OCC_VERSION="v1.2.1" + curl -sfL "https://github.com/openchoreo/openchoreo/releases/download/${OCC_VERSION}/occ_${OCC_VERSION}_linux_amd64.tar.gz" | tar -xz + + ./occ config controlplane update default --url "${OPENCHOREO_API_URL}" + ./occ login --client-credentials + ./occ workload create --descriptor workload.yaml \ + -n "${NAMESPACE}" -p "${PROJECT}" -c "${COMPONENT}" \ + --image "${IMAGE}" -o workload-cr.yaml + ./occ apply -f workload-cr.yaml + ''' + } + } +} +``` + +A few notes: + +- `occ login --client-credentials` reads `OCC_CLIENT_ID` and `OCC_CLIENT_SECRET` from the environment and discovers the token endpoint from the API server, so no IDP URL is needed. See [CLI Configuration](../cli-configuration.mdx) for details. +- Instead of downloading the binary in the stage, you can bake it into your agent image or use `ghcr.io/openchoreo/openchoreo-cli` as the agent. +- `occ apply` replaces the entire workload spec, so keep endpoints and other settings in the `workload.yaml` descriptor (see [Workload Generation](./workload-generation.md)) — an update from a minimal descriptor would remove them. + +The same sequence works from any CI system: export the two environment variables, run `occ login --client-credentials`, and apply the generated Workload CR. + ### GitHub Actions The same two API calls work from a GitHub Actions workflow: build and push the image, then **register the Workload** with OpenChoreo. Store the OAuth client credentials and API/IDP URLs as [repository or organization secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) before using this workflow: diff --git a/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx b/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx index 0ac0f884..a3069331 100644 --- a/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx +++ b/versioned_docs/version-v1.2.x/platform-engineer-guide/workflows/external-ci.mdx @@ -213,6 +213,55 @@ EOF } ``` +#### Using the OpenChoreo CLI instead of curl + +If you would rather not manage raw `curl` calls, the [`occ` CLI](../cli-configuration.mdx) performs the same registration. `occ apply` checks whether the workload exists and creates or updates it, so the 409 fallback disappears, and API errors surface as readable messages with a non-zero exit code — no `jq` needed. The CLI reads its credentials from environment variables and never prints the access token, so the `set +x` guard is also unnecessary. + +Reusing the same Jenkins credentials, the deploy stage becomes: + +```groovy +stage('Deploy to OpenChoreo') { + steps { + withCredentials([ + usernamePassword( + credentialsId: 'workflows-credentials', + usernameVariable: 'OCC_CLIENT_ID', + passwordVariable: 'OCC_CLIENT_SECRET' + ), + string(credentialsId: 'openchoreo-api-url', variable: 'OPENCHOREO_API_URL') + ]) { + sh ''' + set -eu + + # occ stores its login (including the client secret) in + # ${HOME}/.openchoreo/config -- keep it inside the per-build + # workspace on shared agents. + export HOME="${WORKSPACE}" + + # Pin the CLI to the tag matching your OpenChoreo release. + OCC_VERSION="v1.2.1" + curl -sfL "https://github.com/openchoreo/openchoreo/releases/download/${OCC_VERSION}/occ_${OCC_VERSION}_linux_amd64.tar.gz" | tar -xz + + ./occ config controlplane update default --url "${OPENCHOREO_API_URL}" + ./occ login --client-credentials + ./occ workload create --descriptor workload.yaml \ + -n "${NAMESPACE}" -p "${PROJECT}" -c "${COMPONENT}" \ + --image "${IMAGE}" -o workload-cr.yaml + ./occ apply -f workload-cr.yaml + ''' + } + } +} +``` + +A few notes: + +- `occ login --client-credentials` reads `OCC_CLIENT_ID` and `OCC_CLIENT_SECRET` from the environment and discovers the token endpoint from the API server, so no IDP URL is needed. See [CLI Configuration](../cli-configuration.mdx) for details. +- Instead of downloading the binary in the stage, you can bake it into your agent image or use `ghcr.io/openchoreo/openchoreo-cli` as the agent. +- `occ apply` replaces the entire workload spec, so keep endpoints and other settings in the `workload.yaml` descriptor (see [Workload Generation](./workload-generation.md)) — an update from a minimal descriptor would remove them. + +The same sequence works from any CI system: export the two environment variables, run `occ login --client-credentials`, and apply the generated Workload CR. + ### GitHub Actions The same two API calls work from a GitHub Actions workflow: build and push the image, then **register the Workload** with OpenChoreo. Store the OAuth client credentials and API/IDP URLs as [repository or organization secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) before using this workflow: