Install to Claude Code
npx -y skills add https://github.com/kubesphere/kubesphere --skill kubesphere-devops-tenantDescription
If you're a namespace-scoped user in KubeSphere without cluster-admin access, this is how you interact with DevOps projects.
SKILL.md
---
name: kubesphere-devops-tenant
description: Use when operating KubeSphere DevOps as a namespace-scoped tenant with limited permissions, without cluster-admin access, or when accessing DevOps through KubeSphere APIs only
---
# KubeSphere DevOps Tenant Operations
## Overview
This guide covers DevOps operations for **namespace-scoped tenants** who:
- Have admin/operator permissions within their DevOpsProject namespace(s)
- **Cannot** access `kubesphere-devops-system` (Jenkins secrets, tokens)
- **Cannot** call Jenkins APIs directly
- Must use **KubeSphere APIs** (`/kapis/devops.kubesphere.io/`) for all operations
- Use **KubeSphere authentication** (OAuth tokens), not Jenkins tokens
**Critical Distinction:** DevOps projects are **namespaces**, not DevOpsProject CRs. To list accessible DevOps projects:
```bash
# Correct - lists namespaces (DevOps projects) tenant can access
GET /clusters/{cluster}/kapis/devops.kubesphere.io/v1alpha3/workspaces/{workspace}/namespaces
# Wrong - requires cluster-admin, returns 403 for tenants
GET /clusters/{cluster}/apis/devops.kubesphere.io/v1alpha3/devopsprojects
```
## When to Use
- Operating as a project admin/operator (not cluster admin)
- Working within tenant namespace boundaries
- No access to Jenkins secrets in `kubesphere-devops-system`
- Need to trigger pipelines via KubeSphere API
- Building automation for namespace-scoped users
- Developing tenant-facing tooling
## Tenant vs Admin Permissions
| Capability | Tenant (Namespace) | Admin (Cluster) |
|------------|-------------------|-----------------|
| Access DevOpsProject | ✅ Own namespace(s) | ✅ All namespaces |
| Create/Edit Pipelines | ✅ In own namespace | ✅ Any namespace |
| View PipelineRuns | ✅ In own namespace | ✅ Any namespace |
| Access Jenkins Secret | ❌ No | ✅ `kubesphere-devops-system` |
| Direct Jenkins API | ❌ No | ✅ Full access |
| View Jenkins Console | ❌ No | ✅ Via NodePort |
| KubeSphere API | ✅ `/kapis/` | ✅ `/kapis/` |
## Authentication
Tenants authenticate via KubeSphere's OAuth, not Jenkins. See [kubesphere-core](../../core/kubesphere-core/SKILL.md) for complete OAuth authentication details.
### Quick Reference
```bash
# Exchange credentials for OAuth token (see core skill for details)
export KUBESPHERE_API="https://kubesphere-api.example.com"
export USERNAME="tenant-user"
export PASSWORD="tenant-password"
# Get token
export API_TOKEN=$(curl -s -X POST "${KUBESPHERE_API}/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&username=${USERNAME}&password=${PASSWORD}&client_id=kubesphere&client_secret=kubesphere" \
| jq -r '.access_token')
# Use token
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha3/namespaces/demo-project/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}"
```
**Key Points:**
- OAuth token expires in 7200 seconds (2 hours)
- Use `client_id=kubesphere` and `client_secret=kubesphere`
- Token contains user's RBAC permissions
See [kubesphere-core](../../core/kubesphere-core/SKILL.md#authentication) for complete OAuth authentication details including token refresh and common use cases.
### Get KubeSphere API Token
Tenants authenticate via KubeSphere's OAuth, not Jenkins:
```bash
# Method 1: Using kubeconfig (if configured)
kubectl config view --raw -o jsonpath='{.users[?(@.name=="current-user")].user.token}'
# Method 2: Via KubeSphere OAuth API (Recommended)
export KUBESPHERE_URL="https://kubesphere-api.example.com"
export USERNAME="tenant-user"
export PASSWORD="tenant-password"
# Exchange credentials for token
TOKEN_RESPONSE=$(curl -s -X POST "${KUBESPHERE_URL}/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=password" \
--data-urlencode "username=${USERNAME}" \
--data-urlencode "password=${PASSWORD}" \
--data-urlencode "client_id=kubesphere" \
--data-urlencode "client_secret=kubesphere")
# Extract access token
ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
# Token expires in 7200 seconds (2 hours)
echo "Token obtained: ${ACCESS_TOKEN:0:50}..."
```
### Using Token with API
```bash
export API_TOKEN="<your-kubesphere-token>"
export DEVOPS_PROJECT="demo-project"
export KUBESPHERE_API="https://kubesphere-api.example.com"
# Verify access
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}"
```
## Complete Working Example
Here's a verified workflow using tenant credentials (stoneshi / P@88w0rd):
### Step 1: Authenticate
```bash
export KUBESPHERE_API="http://kubesphere-apiserver.kubesphere-system.svc:80"
export USERNAME="stoneshi"
export PASSWORD='P@88w0rd'
# Get OAuth token
TOKEN_RESPONSE=$(curl -s -X POST "${KUBESPHERE_API}/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "username=${USERNAME}" \
-d "password=${PASSWORD}" \
-d "client_id=kubesphere" \
-d "client_secret=kubesphere")
export API_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
echo "Authenticated as: $(curl -s ${KUBESPHERE_API}/kapis/iam.kubesphere.io/v1beta1/users/stoneshi -H "Authorization: Bearer ${API_TOKEN}" | jq -r '.metadata.name')"
```
### Step 2: Access Workspace Resources
```bash
# Verify workspace access (returns "stone")
curl -s "${KUBESPHERE_API}/kapis/tenant.kubesphere.io/v1beta1/workspaces/stone" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.metadata.name'
# Try accessing other workspace (returns 403 Forbidden - correct tenant isolation)
curl -s "${KUBESPHERE_API}/kapis/tenant.kubesphere.io/v1beta1/workspaces/demo" \
-H "Authorization: Bearer ${API_TOKEN}"
# Output: {"message":"workspaces.tenant.kubesphere.io \"demo\" is forbidden..."}
```
### Step 3: Create and List Pipelines
```bash
export DEVOPS_PROJECT="stone-devops" # Must be in "stone" workspace
# List pipelines in tenant namespace
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha3/namespaces/${DEVOPS_PROJECT}/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.items[] | "✓ " + .metadata.name'
# Create pipeline via kubectl (as tenant with namespace permissions)
cat <<EOF | kubectl apply -f -
apiVersion: devops.kubesphere.io/v1alpha3
kind: Pipeline
metadata:
name: stone-tenant-pipeline
namespace: stone-devops
spec:
type: pipeline
pipeline:
name: stone-tenant-pipeline
description: "Test pipeline for tenant verification"
jenkinsfile: |
pipeline {
agent { label "base" }
stages {
stage("Test") {
steps {
sh "echo 'Hello from tenant pipeline'"
}
}
}
}
EOF
# Verify via API
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha3/namespaces/${DEVOPS_PROJECT}/pipelines/stone-tenant-pipeline" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '{name: .metadata.name, type: .spec.type}'
```
### Step 4: Trigger and Monitor Run
```bash
export PIPELINE_NAME="stone-tenant-pipeline"
# Trigger run
curl -s -X POST "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}' | jq -r '{runId: .id, state: .state}'
# List runs (Blue Ocean format)
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs" \
-H "Authorization: Bearer ${API_TOKEN}" | jq '.items[] | {id: .id, state: .state, result: .result}'
# Check specific run status
export RUN_ID="1"
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '{state: .state, result: .result, duration: .durationInMillis}'
# Output: {"state":"FINISHED","result":"SUCCESS","duration":15110}
```
### Step 5: Get Logs
```bash
# Get console log (tenant accessible, no Jenkins token needed)
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}/log" \
-H "Authorization: Bearer ${API_TOKEN}" | tail -20
# Expected output includes:
# + echo Hello from tenant pipeline
# Hello from tenant pipeline
# Finished: SUCCESS
```
### Key Findings
| Aspect | Tenant Behavior |
|--------|-----------------|
| **Authentication** | OAuth with client_id/client_secret = "kubesphere" |
| **Token Expiry** | 7200 seconds (2 hours) |
| **API Version** | v1alpha3 for pipelines, v1alpha2 for runs |
| **Response Format** | Blue Ocean JSON (not Kubernetes resources) |
| **Status Fields** | `.state` (QUEUED/RUNNING/FINISHED), `.result` (SUCCESS/FAILURE) |
| **Namespace Isolation** | 403 Forbidden for other workspaces |
| **Logs Access** | ✅ Available via KubeSphere API |
| **Artifacts** | ✅ Available via `/artifacts` endpoint |
## Pipeline Operations
### List Pipelines (Tenant View)
```bash
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/search?q=type:pipeline" \
-H "Authorization: Bearer ${API_TOKEN}"
# Or list in specific namespace
# Via API (v1alpha3 for pipelines)
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha3/namespaces/${DEVOPS_PROJECT}/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.items[].metadata.name'
-H "Authorization: Bearer ${API_TOKEN}" | jq '.items[].metadata.name'
```
### Get Pipeline Details
```bash
export PIPELINE_NAME="my-pipeline"
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha3/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq .
```
### Create Pipeline (Tenant)
**Regular Pipeline:**
```bash
curl -s -X POST "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "devops.kubesphere.io/v1alpha3",
"kind": "Pipeline",
"metadata": {
"name": "my-tenant-pipeline",
"namespace": "'${DEVOPS_PROJECT}'"
},
"spec": {
"type": "pipeline",
"pipeline": {
"name": "my-tenant-pipeline",
"description": "Pipeline created by tenant",
"jenkinsfile": "pipeline {\n agent { label \"base\" }\n stages {\n stage(\"Build\") {\n steps {\n sh \"echo Building...\"\n }\n }\n }\n}"
}
}
}'
```
**Multi-Branch Pipeline:**
```bash
curl -s -X POST "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "devops.kubesphere.io/v1alpha3",
"kind": "Pipeline",
"metadata": {
"name": "my-multibranch-pipeline",
"namespace": "'${DEVOPS_PROJECT}'"
},
"spec": {
"type": "multi-branch-pipeline",
"multi_branch_pipeline": {
"name": "my-multibranch-pipeline",
"description": "Multi-branch pipeline from tenant",
"source_type": "git",
"git_source": {
"url": "https://github.com/example/repo",
"credential_id": "my-git-credential",
"discover_branches": true,
"discover_tags": false
},
"script_path": "Jenkinsfile"
}
}
}'
```
### Create Multi-Branch Pipeline from Private Repository
> ⚠️ **CRITICAL: Always Check Repository Type First**
>
> Before creating any multi-branch pipeline, you **MUST** ask the user:
> > "Is this a private repository?"
>
> **If YES (Private Repo):**
> 1. Ask if they want to use an existing credential or create a new one
> 2. Create a DevOps credential (`basic-auth` type with GitHub PAT) - see Step 1 below
> 3. Reference the credential in `git_source.credential_id` when creating the pipeline
>
> **If NO (Public Repo):**
> - Set `credential_id: ""` (empty string)
>
> Never assume repository type - always confirm with the user first. Never use `GITHUB_` env vars directly in pipeline specs.
**Complete workflow for private GitHub repository:**
**Prerequisites:**
- GitHub Personal Access Token (PAT) with repo access
- DevOps project namespace (e.g., `devopstestc2nj7`)
**Step 1: Create Credential for GitHub Access**
```bash
export GITHUB_PAT="ghp_xxxxxxxxxxxxxxxxxxxx"
export TENANT_NAME="stone-ns-admin"
curl -s -X POST "${KUBESPHERE_API}/clusters/member-1/kapis/devops.kubesphere.io/v1alpha3/namespaces/devopstestc2nj7/credentials" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "github-token",
"namespace": "devopstestc2nj7",
"annotations": {
"credential.devops.kubesphere.io/type": "basic-auth"
}
},
"stringData": {
"username": "git",
"password": "'${GITHUB_PAT}'"
},
"type": "credential.devops.kubesphere.io/basic-auth"
}'
```
**Step 2: Create GitRepository**
```bash
curl -s -X POST "${KUBESPHERE_API}/clusters/member-1/kapis/devops.kubesphere.io/v1alpha3/namespaces/devopstestc2nj7/gitrepositories" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "devops.kubesphere.io/v1alpha3",
"kind": "GitRepository",
"metadata": {
"name": "my-private-repo",
"namespace": "devopstestc2nj7"
},
"spec": {
"url": "https://github.com/stoneshi-yunify/jenkinsfiles.git",
"provider": "github",
"secret": {
"name": "github-token",
"namespace": "devopstestc2nj7"
},
"description": "Private repository with Jenkinsfile"
}
}'
```
**Step 3: Create Multi-Branch Pipeline**
```bash
curl -s -X POST "${KUBESPHERE_API}/clusters/member-1/kapis/devops.kubesphere.io/v1alpha3/namespaces/devopstestc2nj7/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "devops.kubesphere.io/v1alpha3",
"kind": "Pipeline",
"metadata": {
"name": "echo-pipeline",
"namespace": "devopstestc2nj7",
"annotations": {
"kubesphere.io/creator": "'${TENANT_NAME}'"
}
},
"spec": {
"type": "multi-branch-pipeline",
"multi_branch_pipeline": {
"name": "echo-pipeline",
"description": "Multi-branch pipeline from private repo",
"source_type": "git",
"git_source": {
"url": "https://github.com/stoneshi-yunify/jenkinsfiles.git",
"credential_id": "github-token",
"discover_branches": true,
"discover_tags": false
},
"script_path": "echo/Jenkinsfile"
}
}
}'
```
**Key Points:**
- GitRepository requires `spec.provider` (e.g., `github`) and `spec.secret` fields
- Pipeline **MUST** have `kubesphere.io/creator` annotation when created by tenant
- Multi-branch pipelines auto-discover branches from the repository
## Pipeline Runs (The Tenant Way)
> ⚠️ **API Version Notice**: The `/kapis/devops.kubesphere.io/v1alpha2/` APIs are deprecated. Always prefer `v1alpha3` APIs when available.
### Trigger a Pipeline Run (Multi-Branch)
**For Multi-Branch Pipelines - Three-Step Procedure:**
**Step 1: List Available Branches**
```bash
curl -s "${KUBESPHERE_API}/clusters/${CLUSTER}/kapis/devops.kubesphere.io/v1alpha3/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/branches?filter=origin&page=1&limit=10" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" | jq -r '.items[] | "- Branch: \(.name) | Latest: \(.latestRun.id // "N/A") | Status: \(.latestRun.result // "N/A")"'
```
**Step 2: Ask User Which Branch**
> "Which branch would you like to build?"
**Step 3: Trigger Build with Branch Parameter**
```bash
export BRANCH="main" # User's selection
curl -s -X POST "${KUBESPHERE_API}/clusters/${CLUSTER}/kapis/devops.kubesphere.io/v1alpha3/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/pipelineruns?branch=${BRANCH}" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"parameters":[]}' | jq -r '.metadata.name'
```
**Key Points:**
- Use `v1alpha3` endpoint with `?branch=${BRANCH}` query parameter
- Returns Kubernetes PipelineRun resource (not Blue Ocean format)
- For multi-branch pipelines, the branch parameter is required
### Trigger Repository Scanning (Multi-Branch)
> **Exception to v1alpha3 rule**: Repository scanning uses **v1alpha2** API. This endpoint is not available in v1alpha3.
**When to use:**
- Force immediate repository re-scan to discover new branches
- Troubleshoot branch detection issues
- Manually trigger branch indexing after credential changes
**Step 1: Trigger Scan (v1alpha2)**
```bash
curl -X POST "${KUBESPHERE_API}/clusters/${CLUSTER}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/scan" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'
```
**Step 2: Fetch Scanning Log (v1alpha2)**
```bash
curl -s "${KUBESPHERE_API}/clusters/${CLUSTER}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/consolelog" \
-H "Authorization: Bearer ${API_TOKEN}"
```
**Example scanning output:**
```
Started by user stone-ns-admin
Starting branch indexing...
> git ls-remote --symref -- https://github.com/org/repo.git
Fetching & pruning origin...
Checking branches:
Checking branch main ✓
'Jenkinsfile' found
Met criteria
Checking branch feature-branch
'Jenkinsfile' found
Met criteria
Checking branch old-branch
'Jenkinsfile' not found
Does not meet criteria
Processed 3 branches
Finished branch indexing. Indexing took 3 sec
Finished: SUCCESS
```
**Via PipelineRun CR (kubectl) - Alternative:**
```bash
cat <<EOF | kubectl apply -f -
apiVersion: devops.kubesphere.io/v1alpha3
kind: PipelineRun
metadata:
name: my-run-$(date +%s)
namespace: ${DEVOPS_PROJECT}
spec:
pipelineRef:
name: ${PIPELINE_NAME}
scm:
refName: "main" # Branch name for multi-branch pipelines
refType: "branch"
EOF
```
### List Pipeline Runs
```bash
# Via kubectl (preferred - returns Kubernetes PipelineRun resources)
kubectl get pipelineruns -n ${DEVOPS_PROJECT} --sort-by=.metadata.creationTimestamp
# Via API (v1alpha3 - returns Kubernetes resources)
curl -s "${KUBESPHERE_API}/clusters/${CLUSTER}/kapis/devops.kubesphere.io/v1alpha3/namespaces/${DEVOPS_PROJECT}/pipelineruns?labelSelector=devops.kubesphere.io/pipeline=${PIPELINE_NAME}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq '.items[] | {name: .metadata.name, phase: .status.phase, creationTime: .metadata.creationTimestamp}'
```
### Get Run Status
```bash
# Via kubectl (preferred)
kubectl get pipelinerun ${RUN_NAME} -n ${DEVOPS_PROJECT} -o jsonpath='{.status.phase}'
# Via API (v1alpha3)
curl -s "${KUBESPHERE_API}/clusters/${CLUSTER}/kapis/devops.kubesphere.io/v1alpha3/namespaces/${DEVOPS_PROJECT}/pipelineruns/${RUN_NAME}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '{name: .metadata.name, phase: .status.phase, startTime: .status.startTime, completionTime: .status.completionTime}'
```
### Deprecated v1alpha2 APIs
> ⚠️ **Deprecated**: These v1alpha2 endpoints return Blue Ocean format and are deprecated. Use v1alpha3 APIs shown above.
```bash
# List runs (v1alpha2 - deprecated)
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs" \
-H "Authorization: Bearer ${API_TOKEN}" | jq '.items[] | {id: .id, state: .state, result: .result}'
# Get run status (v1alpha2 - deprecated)
export RUN_ID="1"
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq '{id: .id, state: .state, result: .result, duration: .durationInMillis}'
```
```bash
# Get concise status (Blue Ocean format fields)
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '{id: .id, state: .state, result: .result, startTime: .startTime, duration: .durationInMillis}'
# Example output:
# {
# "id": "1",
# "state": "FINISHED",
# "result": "SUCCESS",
# "startTime": "2026-03-19T02:50:12.747+0000",
# "duration": 15110
# }
# Watch for completion
while true; do
STATUS=$(curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.state')
echo "State: $STATUS"
[[ "$STATUS" == "FINISHED" ]] && break
sleep 5
done
```
```bash
# Get concise status
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '{state: .status.phase, result: .status.conditions[0].reason, startTime: .status.startTime, completionTime: .status.completionTime}'
# Watch for completion
while true; do
STATUS=$(curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.status.phase')
echo "Status: $STATUS"
[[ "$STATUS" == "Succeeded" || "$STATUS" == "Failed" ]] && break
sleep 5
done
```
## Logs and Artifacts (Tenant Access)
### Get Console Log
**Tenant Method (via KubeSphere API):**
```bash
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}/log" \
-H "Authorization: Bearer ${API_TOKEN}"
# Or with kubectl
kubectl get pipelinerun ${RUN_ID} -n ${DEVOPS_PROJECT} -o jsonpath='{.status.log}' 2>/dev/null || echo "Logs via API only"
```
**Note:** Console logs may not be available immediately. Poll until ready:
```bash
while ! curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}/log" \
-H "Authorization: Bearer ${API_TOKEN}" | grep -q "Finished:"; do
echo "Waiting for logs..."
sleep 5
done
echo "Logs ready!"
```
### List Artifacts
```bash
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}/artifacts" \
-H "Authorization: Bearer ${API_TOKEN}" | jq '.[] | {name: .name, path: .path, size: .size}'
```
### Download Artifacts
**Download via KubeSphere API:**
```bash
export ARTIFACT_NAME="service"
export ARTIFACT_PATH="service"
# Get artifact download URL
ARTIFACT_URL=$(curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}/artifacts" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r ".[] | select(.name==\"${ARTIFACT_NAME}\") | .url")
# Download artifact
curl -s "${KUBESPHERE_API}${ARTIFACT_URL}" \
-H "Authorization: Bearer ${API_TOKEN}" \
-o "/tmp/${ARTIFACT_NAME}"
# Verify
ls -lh "/tmp/${ARTIFACT_NAME}"
file "/tmp/${ARTIFACT_NAME}"
```
**Alternative: Via kubectl with exec (if artifact is in workspace):**
```bash
# Find the agent pod (if still running)
AGENT_POD=$(kubectl get pods -n kubesphere-devops-worker -l jenkins/label-digest -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
# Copy artifact (if pod exists)
if [ -n "$AGENT_POD" ]; then
kubectl cp ${AGENT_POD}:/home/jenkins/agent/workspace/${PIPELINE_NAME}/${ARTIFACT_NAME} /tmp/${ARTIFACT_NAME}
fi
```
## Managing Credentials
### List Credentials
```bash
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/credentials" \
-H "Authorization: Bearer ${API_TOKEN}" | jq '.items[].metadata.name'
```
### Create Credential
**SSH Key:**
```bash
curl -s -X POST "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/credentials" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "my-ssh-key",
"namespace": "'${DEVOPS_PROJECT}'",
"annotations": {
"kubesphere.io/creator": "tenant-user",
"kubesphere.io/description": "SSH key for Git"
}
},
"type": "credential.devops.kubesphere.io/ssh",
"stringData": {
"username": "git",
"privateKey": "'$(cat ~/.ssh/id_rsa | sed 's/$/\\n/g' | tr -d '\n')'"
}
}'
```
**Username/Password:**
```bash
curl -s -X POST "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/credentials" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "my-basic-auth",
"namespace": "'${DEVOPS_PROJECT}'"
},
"type": "credential.devops.kubesphere.io/basic-auth",
"stringData": {
"username": "myuser",
"password": "mypassword"
}
}'
```
## Multi-Cluster Operations
KubeSphere supports managing DevOps resources across multiple clusters. Use the `/clusters/{cluster-name}/` prefix to forward API requests to specific member clusters.
### DevOps Projects are Namespaces
**Important:** In KubeSphere DevOps, a "DevOps project" is actually a Kubernetes namespace with the `devops.kubesphere.io/managed=true` label. The DevOpsProject CR is a wrapper resource, but when listing accessible DevOps projects for a tenant, you query **namespaces**, not DevOpsProject CRs.
**Correct API for listing tenant-accessible DevOps projects:**
```bash
# List DevOps project namespaces (NOT devopsprojects CRs)
GET /clusters/{cluster}/kapis/devops.kubesphere.io/v1alpha3/workspaces/{workspace}/namespaces
```
This endpoint returns namespaces that:
1. Have the `devops.kubesphere.io/managed=true` label
2. Have the `kubesphere.io/workspace={workspace}` label
3. Are accessible to the authenticated tenant
### DevOps Project Naming Convention
When users refer to DevOps projects, they may use either a **shortname** or **fullname**:
| Name Type | Example | Source | Description |
|-----------|---------|--------|-------------|
| **Shortname** | `devopstest` | DevOpsProject CR `.metadata.generateName` | User-friendly display name |
| **Fullname** | `devopstestc2nj7` | DevOpsProject CR `.metadata.name` | Actual namespace name |
**Key Points:**
- The **fullname** is the actual Kubernetes namespace name that you use in API calls
- The fullname = DevOpsProject CR's `.metadata.name` = Namespace's `.metadata.name`
- The shortname comes from `.metadata.generateName` and is used for display purposes
**Resolving Ambiguity:**
When a user provides a name that could match multiple projects:
```bash
# First, get all accessible DevOps project namespaces
NAMESPACES=$(curl -s "${KUBESPHERE_API}/clusters/member-1/kapis/devops.kubesphere.io/v1alpha3/workspaces/stone2/namespaces" \
-H "Authorization: Bearer ${API_TOKEN}")
# Example: User says "devopstest" which could match:
# - Fullname: devopstestc2nj7 (from generateName "devopstest")
# - Fullname: devopstestxyz12 (from generateName "devopstest")
# - Fullname: my-devopstest (different project)
# Check for matches
echo "$NAMESPACES" | jq -r '.items[].metadata.name' | grep "devopstest"
# Output might show:
# devopstestc2nj7
# devopstestxyz12
# If multiple matches found, ask user to confirm:
# "Multiple DevOps projects match 'devopstest':
# 1. devopstestc2nj7
# 2. devopstestxyz12
# Which one do you want to use?"
```
**Best Practice:**
1. When user provides a name, check if it matches any fullname (namespace name) exactly
2. If exact match found → use that namespace
3. If no exact match → check if it matches any shortname (generateName prefix)
4. If multiple matches → **ask user to confirm** before proceeding
### API Path Patterns
| Endpoint Type | Path Pattern | Returns | Use Case | Tenant Access |
|--------------|--------------|---------|----------|---------------|
| **KubeSphere API (workspace-scoped)** | `/clusters/{cluster}/kapis/devops.kubesphere.io/v1alpha3/workspaces/{workspace}/namespaces` | **Namespaces** (DevOps projects) | List DevOps project namespaces tenant can access | ✅ **Tenant accessible** |
| **KubeSphere API (namespace-scoped)** | `/clusters/{cluster}/kapis/devops.kubesphere.io/v1alpha3/namespaces/{namespace}/pipelines` | Pipelines | Pipeline operations | ✅ **Tenant accessible** |
| **Kubernetes API (cluster-scoped)** | `/clusters/{cluster}/apis/devops.kubesphere.io/v1alpha3/devopsprojects` | DevOpsProject CRs | Direct CR access | ❌ **Admin only (403)** |
**Key Insight:** The `/kapis/` endpoints enforce workspace-level RBAC and work for tenants. The `/apis/` endpoints require cluster-scoped permissions and will return 403 for tenants. When listing DevOps projects a tenant can access, use the `/namespaces` endpoint, not `/devopsprojects`.
### List DevOpsProjects Across Clusters
```bash
export KUBESPHERE_API="http://kubesphere-apiserver:80"
export USERNAME="stone-ns-admin"
export PASSWORD="P@88w0rd"
export WORKSPACE="stone"
# Get OAuth token
TOKEN=$(curl -s -X POST -H 'Content-Type: application/x-www-form-urlencoded' \
"${KUBESPHERE_API}/oauth/token" \
--data-urlencode 'grant_type=password' \
--data-urlencode "username=${USERNAME}" \
--data-urlencode "password=${PASSWORD}" \
--data-urlencode 'client_id=kubesphere' \
--data-urlencode 'client_secret=kubesphere' | jq -r '.access_token')
# List DevOpsProjects on Host Cluster
echo "=== Host Cluster ==="
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha3/workspaces/${WORKSPACE}/namespaces" \
-H "Authorization: Bearer ${TOKEN}" | jq -r '.items[] | "\(.metadata.name) (\(.metadata.creationTimestamp))"'
# List DevOpsProjects on Member-1 Cluster
echo "=== Member-1 Cluster ==="
curl -s "${KUBESPHERE_API}/clusters/member-1/kapis/devops.kubesphere.io/v1alpha3/workspaces/${WORKSPACE}/namespaces" \
-H "Authorization: Bearer ${TOKEN}" | jq -r '.items[] | "\(.metadata.name) (\(.metadata.creationTimestamp))"'
```
**Example Output:**
```
=== Host Cluster ===
(No output - no DevOpsProjects in workspace 'stone' on host)
=== Member-1 Cluster ===
stonedev154cht (2026-03-18T06:46:04Z)
```
### Why Tenant Can't Use /apis/ Endpoints
```bash
# ❌ This will return 403 Forbidden for tenants
curl -s "${KUBESPHERE_API}/clusters/member-1/apis/devops.kubesphere.io/v1alpha3/devopsprojects" \
-H "Authorization: Bearer ${TOKEN}"
# Output:
# {
# "kind": "Status",
# "apiVersion": "v1",
# "status": "Failure",
# "message": "devopsprojects.devops.kubesphere.io is forbidden: User \"stone-ns-admin\" cannot list resource \"devopsprojects\" in API group \"devops.kubesphere.io\" at the cluster scope",
# "reason": "Forbidden",
# "code": 403
# }
# ✅ This works because /kapis/ with workspace scope enforces tenant RBAC
curl -s "${KUBESPHERE_API}/clusters/member-1/kapis/devops.kubesphere.io/v1alpha3/workspaces/stone/namespaces" \
-H "Authorization: Bearer ${TOKEN}"
# Output:
# {"items":[{"kind":"DevOpsProject","apiVersion":"devops.kubesphere.io/v1alpha3",...}]}
```
### Complete Multi-Cluster Query Script
```bash
#!/bin/bash
export KUBESPHERE_API="http://kubesphere-apiserver:80"
export USERNAME="stone-ns-admin"
export PASSWORD="P@88w0rd"
export WORKSPACE="stone"
# Get token
TOKEN=$(curl -s -X POST -H 'Content-Type: application/x-www-form-urlencoded' \
"${KUBESPHERE_API}/oauth/token" \
--data-urlencode 'grant_type=password' \
--data-urlencode "username=${USERNAME}" \
--data-urlencode "password=${PASSWORD}" \
--data-urlencode 'client_id=kubesphere' \
--data-urlencode 'client_secret=kubesphere' | jq -r '.access_token')
# Get list of clusters (requires admin token or cluster list permission)
# For tenants, typically hardcode the clusters they have access to
CLUSTERS=("host" "member-1")
echo "=== DevOpsProjects in Workspace '${WORKSPACE}' Across All Clusters ==="
for CLUSTER in "${CLUSTERS[@]}"; do
echo -e "\n## Cluster: ${CLUSTER}"
# Use /kapis/ endpoint with workspace scope
ENDPOINT="${KUBESPHERE_API}"
if [ "${CLUSTER}" != "host" ]; then
ENDPOINT="${ENDPOINT}/clusters/${CLUSTER}"
fi
PROJECTS=$(curl -s "${ENDPOINT}/kapis/devops.kubesphere.io/v1alpha3/workspaces/${WORKSPACE}/namespaces" \
-H "Authorization: Bearer ${TOKEN}")
# Check if response contains items
COUNT=$(echo "$PROJECTS" | jq '.items | length')
if [ "$COUNT" -gt 0 ]; then
echo "$PROJECTS" | jq -r '.items[] | " - \(.metadata.name) (Created: \(.metadata.creationTimestamp), Status: \(.metadata.annotations."devopsproject.devops.kubesphere.io/syncstatus" // "N/A"))"'
else
echo " No DevOpsProjects found"
fi
done
```
## Workspace-Scoped API Operations
### Query DevOps Projects (Namespaces) by Workspace
Tenants can query DevOps projects (which are namespaces) within their authorized workspaces:
```bash
# List DevOps project namespaces in specific workspace
# Note: Returns namespaces with devops.kubesphere.io/managed=true label
# Returns empty if tenant doesn't have workspace access
curl -s "${KUBESPHERE_API}/clusters/host/kapis/devops.kubesphere.io/v1alpha3/workspaces/stone/namespaces?sortBy=createTime&limit=10" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.items[].metadata.name'
# Example output:
# stone-devops
# Query different workspace (returns 0 items if no access)
curl -s "${KUBESPHERE_API}/clusters/host/kapis/devops.kubesphere.io/v1alpha3/workspaces/demo/namespaces?sortBy=createTime&limit=10" \
-H "Authorization: Bearer ${API_TOKEN}" | jq '.totalItems'
# Example output:
# 0
```
**Important Distinction:**
- **DevOps Project** = A namespace with `devops.kubesphere.io/managed=true` label
- **DevOpsProject CR** = A Kubernetes custom resource that wraps the namespace
- To list projects a tenant can access → Use `/namespaces` endpoint
- The `/namespaces` endpoint filters by namespace label `kubesphere.io/workspace`. If the namespace label doesn't match the workspace, it won't be returned even if the DevOpsProject CR has the correct label.
### Verify Workspace Access
```bash
# Check accessible workspaces
curl -s "${KUBESPHERE_API}/kapis/tenant.kubesphere.io/v1beta1/workspaces" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.items[].metadata.name'
# Verify specific workspace
curl -s "${KUBESPHERE_API}/kapis/tenant.kubesphere.io/v1beta1/workspaces/stone" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.metadata.name'
```
## GitOps Application Deployment
### Create GitOps Application via API
Tenants can deploy applications using KubeSphere GitOps without accessing the ArgoCD namespace:
```bash
# Create GitOps Application
curl -s -X POST "${KUBESPHERE_API}/kapis/gitops.kubesphere.io/v1alpha1/namespaces/demo-project/applications" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"apiVersion": "gitops.kubesphere.io/v1alpha1",
"kind": "Application",
"metadata": {
"name": "guestbook",
"namespace": "demo-project"
},
"spec": {
"argoApp": {
"spec": {
"project": "default",
"source": {
"repoURL": "https://github.com/stoneshi-yunify/argocd-example-apps",
"targetRevision": "HEAD",
"path": "guestbook"
},
"destination": {
"server": "https://kubernetes.default.svc",
"namespace": "demo-project"
},
"syncPolicy": {
"automated": {
"prune": true,
"selfHeal": true
},
"syncOptions": [
"CreateNamespace=true"
]
}
}
}
}
}' | jq -r '.metadata.name'
# Expected output: guestbook
```
### How It Works
1. **Tenant creates** `Application` (gitops.kubesphere.io/v1alpha1) in their namespace
2. **KubeSphere automatically** creates corresponding ArgoCD Application in `argocd` namespace
3. **ArgoCD controller** syncs the application to tenant's namespace
4. **Tenant cannot** access ArgoCD namespace directly - all operations via KubeSphere API
### Verify Application Deployment
**Method 1: Check Status Labels (Recommended for Tenants)**
The Application resource includes status labels that indicate the current health and sync status:
```bash
# Get Application and check status labels
curl -s "${KUBESPHERE_API}/clusters/member-1/kapis/gitops.kubesphere.io/v1alpha1/namespaces/demo-project/applications/guestbook" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '{
name: .metadata.name,
health: .metadata.labels["gitops.kubesphere.io/health-status"],
sync: .metadata.labels["gitops.kubesphere.io/sync-status"],
argocdApp: .metadata.labels["gitops.kubesphere.io/argocd-application"]
}'
# Expected output when synced and healthy:
# {
# "name": "guestbook",
# "health": "Healthy",
# "sync": "Synced",
# "argocdApp": "guestbook"
# }
```
**Status Values:**
| Label | Values | Description |
|-------|--------|-------------|
| `gitops.kubesphere.io/health-status` | Healthy, Progressing, Degraded, Missing, Unknown | Resource health state |
| `gitops.kubesphere.io/sync-status` | Synced, OutOfSync | Git repository sync state |
**Method 2: Check Detailed Status in .status.argoApp**
For more detailed information, parse the `.status.argoApp` field (JSON string):
```bash
# Get detailed sync and health information
curl -s "${KUBESPHERE_API}/clusters/member-1/kapis/gitops.kubesphere.io/v1alpha1/namespaces/demo-project/applications/guestbook" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.status.argoApp' | jq -r '{
syncStatus: .sync.status,
healthStatus: .health.status,
revision: .sync.revision,
resources: [.resources[] | {kind: .kind, name: .name, status: .status, health: .health.status}],
images: .summary.images
}'
# Expected output:
# {
# "syncStatus": "Synced",
# "healthStatus": "Healthy",
# "revision": "f946a1c393d50a460cc44944a476971fe13961f4",
# "resources": [
# {"kind": "Service", "name": "guestbook-ui", "status": "Synced", "health": "Healthy"},
# {"kind": "Deployment", "name": "guestbook-ui", "status": "Synced", "health": "Healthy"}
# ],
# "images": ["gcr.io/google-samples/gb-frontend:v5"]
# }
```
**Method 3: Check Operation State**
For troubleshooting sync operations:
```bash
# Get operation state and sync result
curl -s "${KUBESPHERE_API}/clusters/member-1/kapis/gitops.kubesphere.io/v1alpha1/namespaces/demo-project/applications/guestbook" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.status.argoApp' | jq -r '.operationState | {
phase: .phase,
message: .message,
startedAt: .startedAt,
finishedAt: .finishedAt
}'
# Expected output on success:
# {
# "phase": "Succeeded",
# "message": "successfully synced (all tasks run)",
# "startedAt": "2026-03-27T09:09:12Z",
# "finishedAt": "2026-03-27T09:09:15Z"
# }
```
**Understanding Destination Cluster**
When `spec.argoApp.spec.destination.server` is `https://kubernetes.default.svc` and `destination.name` is empty or `in-cluster`, the Application deploys to the cluster specified in the API path:
| API Path | Destination Cluster |
|----------|-------------------|
| `/kapis/gitops.kubesphere.io/v1alpha1/namespaces/{ns}/applications` | Host cluster |
| `/clusters/member-1/kapis/gitops.kubesphere.io/v1alpha1/namespaces/{ns}/applications` | member-1 cluster |
| `/clusters/member-2/kapis/gitops.kubesphere.io/v1alpha1/namespaces/{ns}/applications` | member-2 cluster |
**Important for Tenants:**
Since tenants may not have permissions to directly query the destination namespace (due to RBAC), **always verify deployment via the Application status** rather than trying to access deployed resources directly:
```bash
# ✅ CORRECT: Check Application status (tenant has permissions)
curl -s "${KUBESPHERE_API}/clusters/member-1/kapis/gitops.kubesphere.io/v1alpha1/namespaces/demo-project/applications/guestbook" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.metadata.labels["gitops.kubesphere.io/sync-status"]'
# Output: "Synced"
# ❌ INCORRECT: Direct namespace access may fail for tenants
curl -s "${KUBESPHERE_API}/clusters/member-1/api/v1/namespaces/demo-project/pods" \
-H "Authorization: Bearer ${API_TOKEN}"
# May return 403 Forbidden
```
### Tenant Limitations (Important)
| Action | Tenant Can | Notes |
|--------|-----------|-------|
| Create GitOps App | ✅ Yes | Via KubeSphere API |
| Modify ArgoCD Config | ❌ No | Cannot access `argocd` namespace |
| Add App Namespace to ArgoCD | ❌ No | Requires admin to update `application.namespaces` |
| View ArgoCD UI | ❌ No | No direct ArgoCD access |
| View Deployed Resources | ✅ Yes | In own namespace |
## Complete Tenant Workflow
### Step-by-Step: Build and Retrieve Artifacts as Tenant
```bash
#!/bin/bash
set -e
# Configuration
export KUBESPHERE_API="https://kubesphere-api.example.com"
export API_TOKEN="<tenant-token>"
export DEVOPS_PROJECT="demo-project"
export PIPELINE_NAME="my-tenant-pipeline"
# 1. List available pipelines
echo "=== Available Pipelines ==="
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.items[].metadata.name'
# 2. Trigger pipeline run
echo "=== Triggering Pipeline ==="
RUN_RESPONSE=$(curl -s -X POST "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"parameters": [{"name": "BRANCH", "value": "main"}]}')
RUN_ID=$(echo $RUN_RESPONSE | jq -r '.metadata.name')
echo "Run ID: $RUN_ID"
# 3. Wait for completion
echo "=== Waiting for Build ==="
while true; do
STATUS=$(curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}" \
-H "Authorization: Bearer ${API_TOKEN}" | jq -r '.status.phase')
echo "Status: $STATUS"
[[ "$STATUS" == "Succeeded" || "$STATUS" == "Failed" ]] && break
sleep 10
done
# 4. Get logs
echo "=== Console Log ==="
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}/log" \
-H "Authorization: Bearer ${API_TOKEN}" | tail -50
# 5. Download artifacts
echo "=== Downloading Artifacts ==="
ARTIFACTS=$(curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines/${PIPELINE_NAME}/runs/${RUN_ID}/artifacts" \
-H "Authorization: Bearer ${API_TOKEN}")
echo "$ARTIFACTS" | jq -c '.[]' | while read artifact; do
NAME=$(echo $artifact | jq -r '.name')
URL=$(echo $artifact | jq -r '.url')
echo "Downloading: $NAME"
curl -s "${KUBESPHERE_API}${URL}" -H "Authorization: Bearer ${API_TOKEN}" -o "/tmp/${NAME}"
ls -lh "/tmp/${NAME}"
done
echo "=== Done ==="
```
## Tenant Limitations & Workarounds
| Limitation | Tenant Impact | Workaround |
|------------|---------------|------------|
| No Jenkins token | Cannot use Jenkins API directly | Use KubeSphere `/kapis/` endpoints |
| No kubesphere-devops-system access | Cannot view Jenkins master logs | View PipelineRun status via API |
| No agent pod access | Cannot exec into agents | Artifacts via API or pipeline steps |
| Limited logs | Logs may be truncated | Store logs in artifacts or external systems |
| No webhook management | Cannot configure webhooks directly | Use KubeSphere UI or request admin |
### Common Errors and Fixes
**Error: 403 Forbidden**
```bash
# Cause: Token expired or insufficient permissions
# Fix: Refresh token or check RBAC
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha2/namespaces/${DEVOPS_PROJECT}/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}" -v 2>&1 | grep "HTTP/"
# Should be: HTTP/2 200
```
**Error: Resource not found**
```bash
# Cause: Wrong namespace or resource doesn't exist
# Fix: Verify namespace and resource names
kubectl get pipelines -n ${DEVOPS_PROJECT}
kubectl auth can-i get pipelines -n ${DEVOPS_PROJECT}
```
**Error: No logs available**
```bash
# Cause: Run not complete or logs not persisted
# Fix: Wait for completion, check if run succeeded
kubectl get pipelinerun ${RUN_ID} -n ${DEVOPS_PROJECT} -o jsonpath='{.status.phase}'
```
### Workspace API Returns Empty (Namespace Label Mismatch)
**Symptom:**
```bash
# Query workspace API returns 0 items even though DevOpsProject exists
curl -s "${KUBESPHERE_API}/clusters/host/kapis/devops.kubesphere.io/v1alpha3/workspaces/demo/namespaces" \
-H "Authorization: Bearer ${API_TOKEN}"
# Output: {"items": null, "totalItems": 0}
# But direct query works
curl -s "${KUBESPHERE_API}/kapis/devops.kubesphere.io/v1alpha3/namespaces/demo-project/pipelines" \
-H "Authorization: Bearer ${API_TOKEN}"
# Output: Returns pipelines successfully
```
**Root Cause:**
The workspace-scoped API filters namespaces by the label `kubesphere.io/workspace`. If the **namespace** label doesn't match the workspace, it won't be returned, even if the DevOpsProject CR has the correct label.
**Check Labels:**
```bash
# Check DevOpsProject label (usually correct)
kubectl get devopsproject demo-project -o jsonpath='{.metadata.labels.kubesphere\.io/workspace}'
# Output: demo
# Check namespace label (may be empty or wrong)
kubectl get ns demo-project -o jsonpath='{.metadata.labels.kubesphere\.io/workspace}'
# Output: "" (EMPTY - this is the problem!)
```
**Fix (Admin Required):**
```bash
# Update namespace label to match workspace
kubectl label ns demo-project kubesphere.io/workspace=demo --overwrite
# Verify fix
kubectl get ns demo-project -o jsonpath='{.metadata.labels.kubesphere\.io/workspace}'
# Output: demo
# Now workspace API returns the namespace
curl -s "${KUBESPHERE_API}/clusters/host/kapis/devops.kubesphere.io/v1alpha3/workspaces/demo/namespaces" \
-H "Authorization: Bearer ${API_TOKEN}" | jq '.totalItems'
# Output: 1
```
**Why This Happens:**
- DevOpsProject CR and namespace are separate resources
- DevOpsProject controller should sync the workspace label to the namespace
- If the controller missed it or the label was removed, the API filtering breaks
- Workspace-scoped APIs use namespace labels, not DevOpsProject labels
## References
- [KubeSphere DevOps Overview](../kubesphere-devops-overview/SKILL.md)
- [KubeSphere DevOps Pipeline](../kubesphere-devops-pipeline/SKILL.md)
- [DevOps API Documentation](https://docs.kubesphere.io/)
- [RBAC in KubeSphere](https://docs.kubesphere.io/v4.1/05-access-control-and-account-management/)
