Manage ACP Application with run-script Task
Feature Overview
This guide shows how to manage the full lifecycle of an ACP Application — deploy,
update, wait for readiness, roll back, and auto-recover on failure — using the
built-in run-script Task and the kubectl-app-manager tool image.
Why run-script instead of a dedicated Task?
Rather than providing a fixed-parameter Task for each operation, this guide uses the
general-purpose run-script Task paired with the kubectl-app-manager tool image.
This approach gives you full control over the script logic — you can customize
parameters, add conditional branches, combine multiple operations, and adapt to your
specific deployment workflow without waiting for a new Task release.
The trade-off is that you write a short shell script instead of filling in a form.
If this pattern is widely adopted across teams, the scripts can be promoted to a
dedicated catalog Pipeline, eliminating the need to copy them between projects.
Key capabilities
- Deploy, update, wait, and roll back ACP Applications from a pipeline.
- Support both raw Kubernetes manifests and OCI Helm charts (with custom values).
- Cross-cluster / cross-namespace deployment — target any cluster or namespace
by mounting a
kubeconfig Secret, without being limited to the pipeline's own
cluster and namespace.
- Compose individual TaskRuns into a full Pipeline with auto-rollback on failure.
Choose the Delivery Path
Use the following table to pick the pattern that matches your environment and
change scope.
Quick navigation:
About ACP Application
An ACP Application (GVK: applications.app.k8s.io/v1beta1) is a Kubernetes object
that groups a set of related resources — Deployments, Services, ConfigMaps, and more —
under a single lifecycle unit. The platform reconciles the object and reports a unified
health status (Running, Pending, Failed, …) aggregated from the underlying workloads.
The ACP console supports multiple ways to create an Application — from a container
image, from a Helm chart, from raw YAML manifests, from source code (S2I), and from
an Operator-backed service. The kubectl application plugin used in this guide
manages the same underlying Application object. When you create an Application with
--source-type oci, the platform also creates the corresponding HelmRequest
(app.alauda.io/v1) resource internally to reconcile the chart — you do not need to
manage that resource directly.
Key capabilities used in this guide:
Each update operation creates an ApplicationHistory snapshot (GVK:
applicationhistories.app.k8s.io/v1beta1) that can be used for rollback.
What's in the kubectl-app-manager Image
The kubectl-app-manager image is a purpose-built tool image for Application
lifecycle management. It is registered in the catalog and can be selected from the
tool image selector in the run-script Task UI.
Key commands used in this guide:
# Create a new Application from a raw-resource manifest
kubectl application create <appName> -n <namespace> -r resources.yaml
# Update an existing Application
kubectl application update <appName> -n <namespace> -r resources.yaml
# Create from an OCI Helm chart (requires chart-deploy controller in cluster)
kubectl application create <appName> -n <namespace> \
--source-type oci \
--source-address <registry>/<repo> \
-v <chartVersion> \
[-f values.yaml] \
[--source-secret-ref <pullSecret>]
# Poll current state (one-shot, no waiting)
kubectl application status -n <namespace> <appName> --watch=false -o json
# Wait for Running state with timeout
kubectl application status -n <namespace> <appName> --watch=true --timeout=300s -o json
# Roll back to the previous snapshot
kubectl application snapshot rollback -n <namespace> <appName>
For a complete list of available tool images and how to select them, see
Tool Images.
For hub resolver syntax, see
Specifying Remote Tasks Using Hub Resolvers.
Prerequisites
Before you begin
- Tekton Pipelines v1 is installed in your cluster.
- You have a namespace for running pipeline tasks.
- The ACP Application CRD (
applications.app.k8s.io/v1beta1) is installed
(it is part of the ACP platform installation).
- The hub resolver is configured with a catalog named
catalog. Verify with:
kubectl get configmap hubresolver-config -n tekton-pipelines-resolvers \
-o jsonpath='{.data.default-tekton-hub-catalog}'
# Expected output: catalog
RBAC
The ServiceAccount running the pipeline tasks must have permission to manage
Application and ApplicationHistory objects, as well as the underlying Kubernetes
resources (Deployments, Services, etc.).
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: acp-app-manager
namespace: <YOUR_NAMESPACE>
rules:
- apiGroups: [app.k8s.io]
resources: [applications, applicationhistories]
verbs: [get, list, watch, create, update, patch, delete]
- apiGroups: [apps]
resources: [deployments, replicasets, statefulsets, daemonsets]
verbs: [get, list, watch, create, update, patch, delete]
- apiGroups: [""]
resources: [services, configmaps, secrets, serviceaccounts, persistentvolumeclaims]
verbs: [get, list, watch, create, update, patch, delete]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: acp-app-manager
namespace: <YOUR_NAMESPACE>
subjects:
- kind: ServiceAccount
name: <YOUR_SERVICE_ACCOUNT>
namespace: <YOUR_NAMESPACE>
roleRef:
kind: Role
name: acp-app-manager
apiGroup: rbac.authorization.k8s.io
Note: When running through the ACP pipeline gateway, the --as impersonation
flag is not applied to kubectl application sub-commands. Use a ServiceAccount with
the required permissions directly rather than relying on gateway impersonation.
Cross-Cluster / Cross-Namespace Deployment
All scenarios in this guide accept an optional kubeconfigPath argument. When set,
every kubectl application … call targets the cluster and namespace described by that
kubeconfig file instead of the pod's own ServiceAccount credentials.
This lets a single pipeline manage applications across multiple clusters and namespaces
without any code changes — only the binding changes.
Important: For kubectl application ... commands, export KUBECONFIG
instead of prepending --kubeconfig to the command. The plugin rejects global
flags placed before the plugin name.
How to set it up
Step 1 — Store the target cluster kubeconfig in a Secret:
kubectl create secret generic target-cluster-kubeconfig \
--from-file=config=./kubeconfig-target.yaml \
-n <PIPELINE_NAMESPACE>
Step 2 — Bind the Secret to the run-script Task's secret workspace:
workspaces:
- name: secret
secret:
secretName: target-cluster-kubeconfig
Step 3 — Pass the mount path as the kubeconfigPath argument:
- name: args
value:
- <appName>
- <appNamespace>
- /workspace/secret/config # kubeconfigPath
Inside the script, export the path before invoking kubectl application:
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
kubectl application status -n "${APP_NS}" "${APP_NAME}" ...
When kubeconfigPath is empty (the default), the pod uses its own projected
ServiceAccount token — no secret binding needed for same-cluster pipelines.
The following table lists the parameters, workspaces, and results used across the
scenarios in this guide. Naming follows the catalog Task conventions:
params use camelCase, workspaces and results use kebab-case.
Script args (positional)
The run-script Task passes the args array to the script as positional parameters
$1, $2, $3, … in order. For example, args: [demo-app, my-ns, ""] maps to
APP_NAME=$1, APP_NS=$2, KUBECONFIG_PATH=$3 inside the script.
Refer to each scenario's args list for the exact positional mapping.
Workspaces (run-script Task)
Results (run-script Task)
Scenario 1 — Deploy or Update an Application
This script detects whether the Application exists and calls create or update
accordingly. The result payload is written to string-result so downstream tasks
can inspect the action taken.
Raw-resource manifest path (primary)
Use this when your workloads are described as plain Kubernetes manifests. No
chart-deploy engine is required.
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
generateName: deploy-app-
namespace: <YOUR_NAMESPACE>
spec:
serviceAccountName: <YOUR_SERVICE_ACCOUNT>
taskRef:
resolver: hub
params:
- { name: kind, value: task }
- { name: catalog, value: catalog }
- { name: name, value: run-script }
- { name: version, value: "0.1" }
params:
- name: image
value: registry.alauda.cn:60070/devops/tektoncd/hub/kubectl-app-manager:v0.1
- name: imagePullPolicy
value: IfNotPresent
- name: args
value:
- "<APP_NAME>" # 1: appName
- "<APP_NAMESPACE>" # 2: appNamespace
- "" # 3: kubeconfigPath (empty = in-cluster)
- name: script
value: |
# Deploy or update ACP Application from an inline resources manifest.
# Args: 1=appName 2=appNamespace 3=kubeconfigPath
set -euo pipefail
APP_NAME="${1:?appName required}"
APP_NS="${2:?appNamespace required}"
KUBECONFIG_PATH="${3:-}"
# Export KUBECONFIG instead of prepending --kubeconfig because
# kubectl-application rejects global flags before the plugin name.
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
# Keep resource names aligned with appName so users only edit one field
# for the common case where the Application and its main workload share a name.
cat > /tmp/resources.yaml <<RESOURCES_EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP_NAME}
labels:
app.kubernetes.io/name: ${APP_NAME}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: ${APP_NAME}
template:
metadata:
labels:
app.kubernetes.io/name: ${APP_NAME}
spec:
containers:
- name: app
image: <YOUR_IMAGE>
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: ${APP_NAME}
labels:
app.kubernetes.io/name: ${APP_NAME}
spec:
selector:
app.kubernetes.io/name: ${APP_NAME}
ports:
- {port: 80, targetPort: 80, name: http}
RESOURCES_EOF
STATE_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" --watch=false -o json 2>/dev/null || true)
[ -z "${STATE_JSON}" ] && STATE_JSON='{"state":"NotFound"}'
STATE=$(printf '%s' "${STATE_JSON}" | yq e '.state' -)
if [ "${STATE}" = "NotFound" ]; then
kubectl application create "${APP_NAME}" -n "${APP_NS}" -r /tmp/resources.yaml
ACTION="created"
else
kubectl application update "${APP_NAME}" -n "${APP_NS}" -r /tmp/resources.yaml
ACTION="updated"
fi
echo "==> ${ACTION} ${APP_NS}/${APP_NAME}"
printf '{"appNamespace":"%s","appName":"%s","action":"%s"}' \
"${APP_NS}" "${APP_NAME}" "${ACTION}" > "$(results.string-result.path)"
cat > "$(results.overview-markdown.path)" <<EOF
### ACP Application ${ACTION}
| Field | Value |
| --- | --- |
| Namespace | \`${APP_NS}\` |
| Application | \`${APP_NAME}\` |
| Action | \`${ACTION}\` |
EOF
Using a git repository for resources: Instead of inlining the manifest,
bind the source workspace to a git-clone step output and reference the file
path directly:
# When source workspace is bound, the script CWD switches to /workspace/source
kubectl application create "${APP_NAME}" -n "${APP_NS}" -r ./k8s/resources.yaml
OCI Helm Chart path (advanced)
Use this when your application is packaged as an OCI Helm chart. This path requires
the cluster to have a chart-deploy controller (captain) installed and running.
Replace the args and script params in the TaskRun above with the following:
- name: args
value:
- "<APP_NAME>" # 1: appName
- "<APP_NAMESPACE>" # 2: appNamespace
- "<OCI_REGISTRY>/<PROJECT>/<CHART>" # 3: chartAddress (without oci://)
- "<CHART_VERSION>" # 4: chartVersion
- "" # 5: chartPullSecret (empty = public)
- "" # 6: kubeconfigPath (empty = in-cluster)
- "" # 7: valuesYAML (empty = keep current values)
- name: script
value: |
# Deploy or update ACP Application from an OCI Helm chart.
# Args: 1=appName 2=appNamespace 3=chartAddress 4=chartVersion
# 5=chartPullSecret 6=kubeconfigPath 7=valuesYAML
set -euo pipefail
APP_NAME="${1:?appName required}"
APP_NS="${2:?appNamespace required}"
SOURCE_ADDRESS="${3:?chartAddress required}"
CHART_VERSION="${4:?chartVersion required}"
CHART_PULL_SECRET="${5:-}"
KUBECONFIG_PATH="${6:-}"
VALUES_YAML="${7:-}"
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
# Write custom Helm values to a temp file when provided.
VALUES_ARGS=()
if [ -n "${VALUES_YAML}" ]; then
printf '%s' "${VALUES_YAML}" > /tmp/values.yaml
yq eval --inplace --prettyPrint /tmp/values.yaml
echo "==> Custom values:"
cat /tmp/values.yaml
VALUES_ARGS=(-f /tmp/values.yaml)
fi
STATE_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" --watch=false -o json 2>/dev/null || true)
[ -z "${STATE_JSON}" ] && STATE_JSON='{"state":"NotFound"}'
STATE=$(printf '%s' "${STATE_JSON}" | yq e '.state' -)
if [ "${STATE}" = "NotFound" ]; then
PREV_VERSION=""
CREATE_ARGS=(application create "${APP_NAME}" -n "${APP_NS}" \
--source-type oci --source-address "${SOURCE_ADDRESS}" -v "${CHART_VERSION}")
[ -n "${CHART_PULL_SECRET}" ] && CREATE_ARGS+=(--source-secret-ref "${CHART_PULL_SECRET}")
kubectl "${CREATE_ARGS[@]}" ${VALUES_ARGS[@]+"${VALUES_ARGS[@]}"}
ACTION="created"
else
PREV_VERSION=$(kubectl get application.app.k8s.io -n "${APP_NS}" "${APP_NAME}" \
-o jsonpath='{.metadata.annotations.app\.cpaas\.io/chart\.version}' 2>/dev/null || true)
UPDATE_ARGS=(application update "${APP_NAME}" -n "${APP_NS}" \
--source-type oci --source-address "${SOURCE_ADDRESS}" -v "${CHART_VERSION}")
[ -n "${CHART_PULL_SECRET}" ] && UPDATE_ARGS+=(--source-secret-ref "${CHART_PULL_SECRET}")
kubectl "${UPDATE_ARGS[@]}" ${VALUES_ARGS[@]+"${VALUES_ARGS[@]}"}
ACTION="updated"
fi
CURR_VERSION=$(kubectl get application.app.k8s.io -n "${APP_NS}" "${APP_NAME}" \
-o jsonpath='{.metadata.annotations.app\.cpaas\.io/chart\.version}' 2>/dev/null || true)
printf '{"appNamespace":"%s","appName":"%s","action":"%s","previousVersion":"%s","currentVersion":"%s"}' \
"${APP_NS}" "${APP_NAME}" "${ACTION}" "${PREV_VERSION:-}" "${CURR_VERSION:-}" \
> "$(results.string-result.path)"
Requirement: OCI Helm Chart deployment requires the cluster's chart-deploy
(captain) controller to be present. Without it, the Application object is created
but stays Pending indefinitely. Use the raw-resource path if captain is not
installed in your cluster.
HTTP chart repository: If your chart is served from an HTTP repository instead
of an OCI registry, change --source-type oci to --source-type HTTP and set
--source-address to the HTTP repository URL.
Scenario 2 — Wait for the Application to Become Ready
This script uses kubectl application status --watch=true --timeout to wait for the
Application to reach Running state. The plugin handles polling internally and exits
with a non-zero code on timeout, which causes the TaskRun (and any enclosing Pipeline
task) to fail — allowing the finally block to react.
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
generateName: wait-app-
namespace: <YOUR_NAMESPACE>
spec:
serviceAccountName: <YOUR_SERVICE_ACCOUNT>
taskRef:
resolver: hub
params:
- { name: kind, value: task }
- { name: catalog, value: catalog }
- { name: name, value: run-script }
- { name: version, value: "0.1" }
params:
- name: image
value: registry.alauda.cn:60070/devops/tektoncd/hub/kubectl-app-manager:v0.1
- name: imagePullPolicy
value: IfNotPresent
- name: args
value:
- "<APP_NAME>" # 1: appName
- "<APP_NAMESPACE>" # 2: appNamespace
- "" # 3: kubeconfigPath (empty = in-cluster)
- "300" # 4: timeoutSeconds
- name: script
value: |
# Wait for ACP Application to reach Running state.
# Args: 1=appName 2=appNamespace 3=kubeconfigPath 4=timeoutSeconds
set -euo pipefail
APP_NAME="${1:?appName required}"
APP_NS="${2:?appNamespace required}"
KUBECONFIG_PATH="${3:-}"
TIMEOUT="${4:-300}"
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
echo "==> Waiting up to ${TIMEOUT}s for ${APP_NS}/${APP_NAME} to reach Running state"
STATUS_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" \
--timeout="${TIMEOUT}s" --watch=true -o json || true)
STATE=$(printf '%s' "${STATUS_JSON}" | yq e '.state' -)
printf '%s' "${STATE}" > "$(results.string-result.path)"
if [ "${STATE}" != "Running" ]; then
echo "ERROR: ${APP_NS}/${APP_NAME} did not reach Running (last state: ${STATE})"
exit 1
fi
echo "Application ${APP_NS}/${APP_NAME} is Running."
Expected output — normal path:
==> Waiting up to 300s for devops-doc-test/demo-app to reach Running state
Application devops-doc-test/demo-app is Running.
Expected output — timeout path:
==> Waiting up to 30s for devops-doc-test/demo-app to reach Running state
ERROR: devops-doc-test/demo-app did not reach Running (last state: Pending)
Scenario 3 — Rollback to the Previous Snapshot
This script triggers kubectl application snapshot rollback which reverts the
Application to its most recent ApplicationHistory snapshot, then waits for the
Application to return to Running.
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
generateName: rollback-app-
namespace: <YOUR_NAMESPACE>
spec:
serviceAccountName: <YOUR_SERVICE_ACCOUNT>
taskRef:
resolver: hub
params:
- { name: kind, value: task }
- { name: catalog, value: catalog }
- { name: name, value: run-script }
- { name: version, value: "0.1" }
params:
- name: image
value: registry.alauda.cn:60070/devops/tektoncd/hub/kubectl-app-manager:v0.1
- name: imagePullPolicy
value: IfNotPresent
- name: args
value:
- "<APP_NAME>" # 1: appName
- "<APP_NAMESPACE>" # 2: appNamespace
- "" # 3: kubeconfigPath (empty = in-cluster)
- "300" # 4: timeoutSeconds
- name: script
value: |
# Rollback ACP Application to its previous snapshot.
# Args: 1=appName 2=appNamespace 3=kubeconfigPath 4=timeoutSeconds
set -euo pipefail
APP_NAME="${1:?appName required}"
APP_NS="${2:?appNamespace required}"
KUBECONFIG_PATH="${3:-}"
TIMEOUT="${4:-300}"
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
echo "==> Triggering snapshot rollback for ${APP_NS}/${APP_NAME}"
if ! kubectl application snapshot rollback -n "${APP_NS}" "${APP_NAME}"; then
echo "==> Rollback command failed (application may have no previous snapshot)."
printf 'NoSnapshot' > "$(results.string-result.path)"
exit 0
fi
echo "==> Rollback triggered; waiting up to ${TIMEOUT}s for Running state"
STATUS_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" \
--timeout="${TIMEOUT}s" --watch=true -o json || true)
STATE=$(printf '%s' "${STATUS_JSON}" | yq e '.state' -)
printf '%s' "${STATE}" > "$(results.string-result.path)"
if [ "${STATE}" != "Running" ]; then
echo "ERROR: rollback failed, ${APP_NS}/${APP_NAME} entered state: ${STATE}"
exit 1
fi
echo "==> Rollback succeeded; ${APP_NS}/${APP_NAME} is Running."
Expected output:
==> Triggering snapshot rollback for devops-doc-test/demo-app
==> Rollback triggered; waiting up to 300s for Running state
==> Rollback succeeded; devops-doc-test/demo-app is Running.
To list available snapshots:
# List all ApplicationHistory snapshots for a namespace
kubectl get applicationhistory -n <namespace>
# Rollback rolls back to the previous (n-1) snapshot by default.
kubectl application snapshot rollback -n <namespace> <appName>
Scenario 4 — Production Pipeline: Deploy, Wait, and Auto-Rollback
This Pipeline combines the three scripts above into a production-ready deploy flow:
deploy-app — creates or updates the Application
wait-app — waits until Running or times out (runs after deploy-app)
rollback-app (in finally) — rolls back automatically if wait-app failed
The when guard on the finally task ensures rollback only fires on failure.
retries: 2 gives the rollback two additional attempts if the first one fails.
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: acp-app-deploy-wait-rollback
namespace: <YOUR_NAMESPACE>
spec:
params:
- name: appName
type: string
description: Name of the ACP Application object.
- name: appNamespace
type: string
description: Namespace where the ACP Application lives.
- name: resourcesYAML
type: string
description: |
Multi-document YAML string containing all Kubernetes resources
(Deployments, Services, ConfigMaps, …) to associate with the Application.
- name: waitTimeout
type: string
default: "300"
description: Seconds to wait for the Application to reach Running state.
- name: rollbackTimeout
type: string
default: "300"
description: Seconds to wait for the rollback to complete.
- name: scriptImage
type: string
default: registry.alauda.cn:60070/devops/tektoncd/hub/kubectl-app-manager:v0.1
description: |
Tool image containing kubectl and the kubectl-application plugin.
Pin to a specific tag (e.g. v0.1) for reproducible runs.
If using imagePullPolicy: IfNotPresent, ensure the tag is pinned — using
'latest' with IfNotPresent may serve a stale cached image after upgrades.
- name: kubeconfigPath
type: string
default: ""
description: |
Path to a kubeconfig file inside the task pod. Leave empty to use
the pod's in-cluster ServiceAccount credentials.
Typically /workspace/secret/config when the kubeconfig workspace is bound.
workspaces:
- name: kubeconfig
optional: true
description: |
Optional Secret containing a kubeconfig file for cross-cluster targeting.
When bound, the file is available at /workspace/secret/<key> inside the task
pod (because it maps to the run-script Task's 'secret' workspace).
Set kubeconfigPath to /workspace/secret/<key>.
tasks:
- name: deploy-app
taskRef:
resolver: hub
params:
- { name: kind, value: task }
- { name: catalog, value: catalog }
- { name: name, value: run-script }
- { name: version, value: "0.1" }
workspaces:
- name: secret
workspace: kubeconfig
params:
- name: image
value: $(params.scriptImage)
- name: imagePullPolicy
value: IfNotPresent
- name: args
value:
- $(params.appName)
- $(params.appNamespace)
- $(params.kubeconfigPath)
- name: script
value: |
set -euo pipefail
APP_NAME="${1:?appName required}"
APP_NS="${2:?appNamespace required}"
KUBECONFIG_PATH="${3:-}"
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
cat > /tmp/resources.yaml <<'RESOURCES_EOF'
$(params.resourcesYAML)
RESOURCES_EOF
STATE_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" --watch=false -o json 2>/dev/null || true)
[ -z "${STATE_JSON}" ] && STATE_JSON='{"state":"NotFound"}'
STATE=$(printf '%s' "${STATE_JSON}" | yq e '.state' -)
if [ "${STATE}" = "NotFound" ]; then
kubectl application create "${APP_NAME}" -n "${APP_NS}" -r /tmp/resources.yaml
ACTION="created"
else
kubectl application update "${APP_NAME}" -n "${APP_NS}" -r /tmp/resources.yaml
ACTION="updated"
fi
echo "==> ${ACTION} ${APP_NS}/${APP_NAME}"
printf '{"appNamespace":"%s","appName":"%s","action":"%s"}' \
"${APP_NS}" "${APP_NAME}" "${ACTION}" > "$(results.string-result.path)"
- name: wait-app
runAfter: [deploy-app]
taskRef:
resolver: hub
params:
- { name: kind, value: task }
- { name: catalog, value: catalog }
- { name: name, value: run-script }
- { name: version, value: "0.1" }
workspaces:
- name: secret
workspace: kubeconfig
params:
- name: image
value: $(params.scriptImage)
- name: imagePullPolicy
value: IfNotPresent
- name: args
value:
- $(params.appName)
- $(params.appNamespace)
- $(params.kubeconfigPath)
- $(params.waitTimeout)
- name: script
value: |
set -euo pipefail
APP_NAME="${1:?appName required}"
APP_NS="${2:?appNamespace required}"
KUBECONFIG_PATH="${3:-}"
TIMEOUT="${4:-300}"
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
echo "==> Waiting up to ${TIMEOUT}s for ${APP_NS}/${APP_NAME} to reach Running state"
STATUS_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" \
--timeout="${TIMEOUT}s" --watch=true -o json || true)
STATE=$(printf '%s' "${STATUS_JSON}" | yq e '.state' -)
printf '%s' "${STATE}" > "$(results.string-result.path)"
if [ "${STATE}" != "Running" ]; then
echo "ERROR: ${APP_NS}/${APP_NAME} did not reach Running (last state: ${STATE})"
exit 1
fi
echo "Application ${APP_NS}/${APP_NAME} is Running."
finally:
- name: rollback-app
# Only trigger when wait-app failed — skip on a clean success path.
when:
- input: $(tasks.wait-app.status)
operator: in
values: ["Failed"]
taskRef:
resolver: hub
params:
- { name: kind, value: task }
- { name: catalog, value: catalog }
- { name: name, value: run-script }
- { name: version, value: "0.1" }
retries: 2
workspaces:
- name: secret
workspace: kubeconfig
params:
- name: image
value: $(params.scriptImage)
- name: imagePullPolicy
value: IfNotPresent
- name: args
value:
- $(params.appName)
- $(params.appNamespace)
- $(params.kubeconfigPath)
- $(params.rollbackTimeout)
- name: script
value: |
set -euo pipefail
APP_NAME="${1:?appName required}"
APP_NS="${2:?appNamespace required}"
KUBECONFIG_PATH="${3:-}"
TIMEOUT="${4:-300}"
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
echo "==> Triggering snapshot rollback for ${APP_NS}/${APP_NAME}"
if ! kubectl application snapshot rollback -n "${APP_NS}" "${APP_NAME}"; then
echo "==> Rollback command failed (application may have no previous snapshot)."
printf 'NoSnapshot' > "$(results.string-result.path)"
exit 0
fi
STATUS_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" \
--timeout="${TIMEOUT}s" --watch=true -o json || true)
STATE=$(printf '%s' "${STATUS_JSON}" | yq e '.state' -)
printf '%s' "${STATE}" > "$(results.string-result.path)"
if [ "${STATE}" != "Running" ]; then
echo "ERROR: rollback failed, ${APP_NS}/${APP_NAME} entered state: ${STATE}"
exit 1
fi
echo "==> Rollback succeeded; ${APP_NS}/${APP_NAME} is Running."
Running the Pipeline
Happy path — good image, Application reaches Running:
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: acp-app-deploy-
namespace: <YOUR_NAMESPACE>
spec:
taskRunTemplate:
serviceAccountName: <YOUR_SERVICE_ACCOUNT>
pipelineRef:
name: acp-app-deploy-wait-rollback
params:
- name: appName
value: my-app
- name: appNamespace
value: <YOUR_NAMESPACE>
- name: waitTimeout
value: "300"
- name: rollbackTimeout
value: "300"
- name: resourcesYAML
value: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
labels:
app.kubernetes.io/name: my-app
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: my-app
template:
metadata:
labels:
app.kubernetes.io/name: my-app
spec:
containers:
- name: app
image: <YOUR_IMAGE>:<YOUR_TAG>
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
selector:
app.kubernetes.io/name: my-app
ports:
- {port: 80, targetPort: 8080, name: http}
Expected result: Tasks Completed: 2 (Failed: 0), Skipped: 1
(rollback-app is skipped because wait-app succeeded.)
Failure path — bad image, wait-app times out, rollback fires:
To test this path, change the resourcesYAML param to use a non-existent image tag
and reduce waitTimeout so the failure surfaces quickly:
params:
- name: appName
value: my-app
- name: appNamespace
value: <YOUR_NAMESPACE>
- name: waitTimeout
value: "60" # short timeout so the test completes quickly
- name: rollbackTimeout
value: "300"
- name: resourcesYAML
value: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
image: <YOUR_REGISTRY>/my-app:nonexistent-tag # intentionally bad
The pipeline will:
deploy-app — succeeds (writes the bad spec to the Application)
wait-app — fails after waitTimeout seconds (Application stays Pending)
rollback-app (finally) — triggers, rolls back to the previous snapshot
Expected result: Tasks Completed: 3 (Failed: 1), Skipped: 0
The Application returns to Running after the rollback completes.
Cross-cluster variant
To target a different cluster, bind the kubeconfig workspace and set
kubeconfigPath to the mounted file path:
spec:
taskRunTemplate:
serviceAccountName: <YOUR_SERVICE_ACCOUNT>
workspaces:
- name: kubeconfig # matches the Pipeline workspace name
secret:
secretName: target-cluster-kubeconfig
params:
- name: kubeconfigPath
value: /workspace/secret/config # mounted by run-script's 'secret' workspace
# ... other params unchanged
The Pipeline maps its kubeconfig workspace to the secret workspace of each
task. The file is available at /workspace/secret/<key> inside the task pod, where
<key> is the Secret key name you used when creating the Secret
(e.g., config for --from-file=config=./kubeconfig-target.yaml).
Scenario 5 — Generic Manifest Patch (Fallback)
Use this when you need a fine-grained field update that kubectl application update
does not cover — for example, patching a single environment variable or annotation
without replacing the entire resource manifest.
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
generateName: patch-app-resource-
namespace: <YOUR_NAMESPACE>
spec:
serviceAccountName: <YOUR_SERVICE_ACCOUNT>
taskRef:
resolver: hub
params:
- { name: kind, value: task }
- { name: catalog, value: catalog }
- { name: name, value: run-script }
- { name: version, value: "0.1" }
params:
- name: image
value: registry.alauda.cn:60070/devops/tektoncd/hub/kubectl-app-manager:v0.1
- name: imagePullPolicy
value: IfNotPresent
- name: args
value:
- "<APP_NAME>" # 1: appName
- "<APP_NAMESPACE>" # 2: appNamespace
- "" # 3: kubeconfigPath (empty = in-cluster)
- "Deployment" # 4: resourceKind
- "<RESOURCE_NAME>" # 5: resourceName (defaults to appName if empty)
- name: script
value: |
# Generic JSON merge patch on a resource owned by an ACP Application.
# Args: 1=appName 2=appNamespace 3=kubeconfigPath 4=resourceKind 5=resourceName
set -euo pipefail
APP_NAME="${1:?appName required}"
APP_NS="${2:?appNamespace required}"
KUBECONFIG_PATH="${3:-}"
RESOURCE_KIND="${4:-Deployment}"
RESOURCE_NAME="${5:-${APP_NAME}}"
[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
# Customize this patch JSON for your use case.
# --type=merge uses RFC 7386 JSON Merge Patch: scalar fields are merged,
# but arrays are REPLACED entirely. For array-safe patching (e.g., updating
# one container in a multi-container pod), use --type=strategic instead.
# Examples:
# Scale replicas: '{"spec":{"replicas":3}}'
# Add annotation: '{"metadata":{"annotations":{"deploy.env":"staging"}}}'
PATCH='{"spec":{"replicas":3}}'
echo "==> Patching ${RESOURCE_KIND} ${APP_NS}/${RESOURCE_NAME}"
kubectl patch "${RESOURCE_KIND}" "${RESOURCE_NAME}" \
-n "${APP_NS}" --type=merge --patch "${PATCH}"
printf '{"namespace":"%s","kind":"%s","name":"%s"}' \
"${APP_NS}" "${RESOURCE_KIND}" "${RESOURCE_NAME}" \
> "$(results.string-result.path)"
Trigger from a New Artifact
You can wire Scenario 4's Pipeline to run automatically whenever a new OCI image or
Helm chart is pushed to a registry, by using the ACP artifact trigger mechanism.
The trigger generates a PipelineRun from a template and injects artifact metadata
(registry URL, digest, tag) as parameters:
# PipelineRun template — placeholders are filled by the artifact trigger
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: acp-app-deploy-
namespace: <YOUR_NAMESPACE>
spec:
taskRunTemplate:
serviceAccountName: <YOUR_SERVICE_ACCOUNT>
pipelineRef:
name: acp-app-deploy-wait-rollback
params:
- name: appName
value: my-app
- name: appNamespace
value: <YOUR_NAMESPACE>
- name: waitTimeout
value: "300"
- name: rollbackTimeout
value: "300"
- name: resourcesYAML
value: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
template:
spec:
containers:
- name: app
# $(ARTIFACT_URL) and $(ARTIFACT_TAG) are injected by the trigger
image: $(ARTIFACT_URL):$(ARTIFACT_TAG)
For full trigger configuration, see the ACP artifact trigger documentation in your
platform's how-to guides.
Validation
After submitting a PipelineRun or TaskRun, check progress with:
# List recent PipelineRuns (newest last)
kubectl get pipelinerun -n <namespace> --sort-by=.metadata.creationTimestamp
# List TaskRuns created by a specific PipelineRun
kubectl get taskrun -n <namespace> -l tekton.dev/pipelineRun=<pipelineRunName>
# Check Application state (requires kubectl-application plugin installed locally)
kubectl application status -n <namespace> <appName> --watch=false -o json
# Without the plugin, check the ACP Application object directly:
kubectl get application.app.k8s.io -n <namespace> <appName> -o jsonpath='{.status}'
# Inspect task results from a TaskRun (use the name from the list above)
kubectl get taskrun <taskRunName> -n <namespace> \
-o jsonpath='{.status.taskResults}'
# View Application history snapshots
kubectl get applicationhistory -n <namespace>
Manual Rollback
To roll back an Application outside of a pipeline:
# List available snapshots
kubectl get applicationhistory -n <namespace>
# Roll back to the previous (n-1) snapshot
kubectl application snapshot rollback -n <namespace> <appName>
# Verify the result
kubectl application status -n <namespace> <appName> --watch=false -o json
snapshot rollback always rolls back to the most recent previous snapshot.
To target a specific snapshot, inspect the ApplicationHistory objects and
use kubectl apply to restore the specific resource spec from that snapshot's
.spec.resources field directly.
Troubleshooting
Forbidden / permission errors
Ensure the ServiceAccount has a Role with applications and applicationhistories
permissions in the target namespace. See the Prerequisites section.
The ACP pipeline gateway's --as impersonation does not propagate to
kubectl application sub-commands. Assign permissions to the ServiceAccount
directly.
Application stays Pending after OCI chart deploy
This means the cluster does not have a chart-deploy (captain) controller. Either:
- Switch to the raw-resource manifest path (
-r resources.yaml), or
- Install the chart-deploy controller in your cluster.
Chart pull secret not taking effect
Ensure the Secret is in the same namespace as the Application, and that its name
matches the --source-secret-ref argument exactly. Verify with:
kubectl get secret <pullSecret> -n <namespace>
wait-app times out unexpectedly
Check the underlying workload state directly:
kubectl get deployment <appName> -n <namespace>
kubectl describe pod -n <namespace> -l app.kubernetes.io/name=<appName>
Common causes: image pull errors, readiness probe failures, resource quota exceeded.
rollback-app (finally) did not trigger
The finally task's when condition is:
- input: $(tasks.wait-app.status)
operator: in
values: ["Failed"]
This only matches when wait-app actually ran and failed. If wait-app was
Skipped (e.g., because deploy-app also failed), Tekton evaluates
$(tasks.wait-app.status) as None, which does not match "Failed", so
rollback-app is also skipped.
Verify by inspecting the PipelineRun:
kubectl get pipelinerun <pipelinerunName> -n <namespace> \
-o jsonpath='{.status.childReferences[*]}'
rollback-app skipped — no previous snapshot exists
This happens on the very first deployment: the Application was just created and has
no history yet. The rollback script catches the failure from
kubectl application snapshot rollback and exits cleanly with NoSnapshot written
to string-result.
In this case the PipelineRun ends with Failed status from wait-app, but
rollback-app completes successfully (skip). Fix the root cause of the deployment
failure, then manually delete the broken Application and re-run.
deploy-app itself failed — rollback did not trigger
If deploy-app fails (e.g., due to an RBAC error or network issue), wait-app
is Skipped rather than Failed. The when condition on rollback-app checks
$(tasks.wait-app.status) == Failed, so rollback is also skipped.
In this case the Application was not updated, so no rollback is needed. Fix the root
cause of the deploy-app failure and re-run the PipelineRun.
Cross-cluster kubeconfig invalid or expired
If the kubeconfig Secret was rotated or the certificate expired, the task will fail
with an authentication error. Recreate the Secret with a fresh kubeconfig:
kubectl delete secret target-cluster-kubeconfig -n <namespace>
kubectl create secret generic target-cluster-kubeconfig \
--from-file=config=./kubeconfig-new.yaml \
-n <namespace>
See Also