diff --git a/.gitea/actions/extract-namespace-from-repo-name/action.yaml b/.gitea/actions/extract-namespace-from-repo-name/action.yaml new file mode 100644 index 0000000..d8a639e --- /dev/null +++ b/.gitea/actions/extract-namespace-from-repo-name/action.yaml @@ -0,0 +1,19 @@ +name: "Extract namespace from repo name" +description: "Extracts the namespace name from the repo name, based on the convention of k_" +inputs: + repo: + description: 'The repo name, get it from "github.repository"' + required: true +outputs: + namespace: + description: "The namespace name" + value: ${{ steps.extract.outputs.suffix }} +runs: + using: "composite" + steps: + - id: extract + shell: bash + run: | + full_repo="${{ inputs.repo }}" + suffix="${full_repo##*k_}" + echo "suffix=$suffix" >> $GITHUB_OUTPUT diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml new file mode 100644 index 0000000..a9a24fd --- /dev/null +++ b/.gitea/workflows/cd.yaml @@ -0,0 +1,197 @@ +name: Deploy + +on: + workflow_call: + inputs: + # Optional: Override the default k8s directory path + k8s_dir: + description: "Path to Kubernetes manifests directory" + required: false + default: "k8s/" + type: string + # Optional: Override the default helmfile path + helmfile_path: + description: "Path to helmfile.yaml" + required: false + default: "helmfile.yaml" + type: string + # Optional: Skip Helm deployment even if helmfile exists + skip_helm_deployment: + description: "Skip Helm deployment even if helmfile.yaml exists" + required: false + default: false + type: boolean + # Optional: Custom secrets to create (JSON array of secret objects) + custom_secrets: + description: "JSON array of secrets to create. Each secret should have: name, type, data" + required: false + default: "[]" + type: string + # Optional: Branch to deploy from + deploy_branch: + description: "Branch to deploy from" + required: false + default: "main" + type: string + +jobs: + detect-service-type: + runs-on: ubuntu-latest + outputs: + has_helmfile: ${{ steps.check-helmfile.outputs.exists }} + has_k8s: ${{ steps.check-k8s.outputs.exists }} + steps: + - uses: actions/checkout@v4 + - name: Check if helmfile.yaml exists + id: check-helmfile + run: | + if [ -f "${{ inputs.helmfile_path }}" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Found helmfile.yaml at ${{ inputs.helmfile_path }}" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "No helmfile.yaml found at ${{ inputs.helmfile_path }}" + fi + - name: Check if k8s directory exists + id: check-k8s + run: | + if [ -d "${{ inputs.k8s_dir }}" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Found k8s directory at ${{ inputs.k8s_dir }}" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "No k8s directory found at ${{ inputs.k8s_dir }}" + fi + + deploy-k8s: + runs-on: ubuntu-latest + needs: detect-service-type + if: needs.detect-service-type.outputs.has_k8s == 'true' + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.deploy_branch }} + - uses: ./.gitea/actions/extract-namespace-from-repo-name + id: namespace + with: + repo: ${{ github.repository }} + - uses: azure/setup-kubectl@v4 + - uses: azure/k8s-set-context@v4 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBECONFIG }} + - name: Create custom secrets + id: create-secrets + run: | + # Parse custom secrets from input + SECRETS='${{ inputs.custom_secrets }}' + if [ "$SECRETS" != "[]" ]; then + echo "Creating custom secrets..." + echo "$SECRETS" | jq -c '.[]' | while read -r secret; do + SECRET_NAME=$(echo "$secret" | jq -r '.name') + SECRET_TYPE=$(echo "$secret" | jq -r '.type // "generic"') + SECRET_DATA=$(echo "$secret" | jq -r '.data') + + echo "Creating secret: $SECRET_NAME (type: $SECRET_TYPE)" + + # Create the secret using kubectl + echo "$SECRET_DATA" | kubectl create secret $SECRET_TYPE $SECRET_NAME \ + --from-literal=secret.json="$SECRET_DATA" \ + --namespace=${{ steps.namespace.outputs.namespace }} \ + --dry-run=client -o yaml | kubectl apply -f - + done + else + echo "No custom secrets to create" + fi + - name: Deploy Kubernetes manifests + uses: azure/k8s-deploy@v5.0.4 + with: + action: deploy + manifests: "${{ inputs.k8s_dir }}" + strategy: basic + namespace: ${{ steps.namespace.outputs.namespace }} + + deploy-helm: + runs-on: ubuntu-latest + needs: detect-service-type + if: | + needs.detect-service-type.outputs.has_helmfile == 'true' && + needs.detect-service-type.outputs.has_k8s == 'true' && + inputs.skip_helm_deployment != 'true' + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.deploy_branch }} + - uses: ./.gitea/actions/extract-namespace-from-repo-name + id: namespace + with: + repo: ${{ github.repository }} + - uses: azure/setup-kubectl@v4 + - uses: azure/setup-helm@v4 + - uses: azure/k8s-set-context@v4 + with: + method: kubeconfig + kubeconfig: ${{ secrets.KUBECONFIG }} + - name: Create custom secrets + id: create-secrets + run: | + # Parse custom secrets from input + SECRETS='${{ inputs.custom_secrets }}' + if [ "$SECRETS" != "[]" ]; then + echo "Creating custom secrets..." + echo "$SECRETS" | jq -c '.[]' | while read -r secret; do + SECRET_NAME=$(echo "$secret" | jq -r '.name') + SECRET_TYPE=$(echo "$secret" | jq -r '.type // "generic"') + SECRET_DATA=$(echo "$secret" | jq -r '.data') + + echo "Creating secret: $SECRET_NAME (type: $SECRET_TYPE)" + + # Create the secret using kubectl + echo "$SECRET_DATA" | kubectl create secret $SECRET_TYPE $SECRET_NAME \ + --from-literal=secret.json="$SECRET_DATA" \ + --namespace=${{ steps.namespace.outputs.namespace }} \ + --dry-run=client -o yaml | kubectl apply -f - + done + else + echo "No custom secrets to create" + fi + - name: Deploy Helm + uses: helmfile/helmfile-action@v2 + with: + helmfile-args: apply + + # Summary job that always runs to show what was deployed + deployment-summary: + runs-on: ubuntu-latest + needs: [detect-service-type, deploy-k8s, deploy-helm] + if: always() + steps: + - name: Deployment Summary + run: | + echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.detect-service-type.outputs.has_k8s }}" == "true" ]; then + echo "✅ **Kubernetes deployment**: Completed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Kubernetes deployment**: Skipped (no k8s/ directory found)" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.detect-service-type.outputs.has_helmfile }}" == "true" ] && [ "${{ inputs.skip_helm_deployment }}" != "true" ]; then + echo "✅ **Helm deployment**: Completed" >> $GITHUB_STEP_SUMMARY + elif [ "${{ needs.detect-service-type.outputs.has_helmfile }}" == "true" ] && [ "${{ inputs.skip_helm_deployment }}" == "true" ]; then + echo "⏭️ **Helm deployment**: Skipped (manually disabled)" >> $GITHUB_STEP_SUMMARY + else + echo "⏭️ **Helm deployment**: Skipped (no helmfile.yaml found)" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Service Type**: ${{ needs.detect-service-type.outputs.has_helmfile == 'true' && 'Helm + Kubernetes' || 'Kubernetes Only' }}" >> $GITHUB_STEP_SUMMARY + + # Show custom secrets info + SECRETS='${{ inputs.custom_secrets }}' + if [ "$SECRETS" != "[]" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Custom Secrets Created**: $(echo "$SECRETS" | jq length)" >> $GITHUB_STEP_SUMMARY + echo "$SECRETS" | jq -r '.[] | "- " + .name + " (" + (.type // "generic") + ")"' >> $GITHUB_STEP_SUMMARY + fi diff --git a/CD-README.md b/CD-README.md new file mode 100644 index 0000000..1f33d54 --- /dev/null +++ b/CD-README.md @@ -0,0 +1,277 @@ +# Reusable CD Workflow for Kubernetes Services + +This directory contains a reusable CD (Continuous Deployment) workflow that automatically detects and deploys your Kubernetes services, whether they use Helm + Kubernetes or just Kubernetes manifests, with flexible secret management. + +## Features + +- **Automatic Detection**: Automatically detects if your service uses Helm (helmfile.yaml) or just Kubernetes manifests +- **Conditional Deployment**: Only runs Helm deployment when helmfile.yaml exists +- **Flexible Paths**: Configurable paths for k8s directory and helmfile +- **Smart Secret Management**: Flexible JSON-based secret creation system +- **Branch Flexibility**: Deploy from any branch (defaults to main) +- **Comprehensive Summary**: Provides a clear summary of what was deployed + +## Usage + +### Basic Usage (Recommended) + +Simply call the workflow without any parameters - it will automatically detect your service type: + +```yaml +jobs: + deploy: + uses: ./.gitea/workflows/cd.yaml +``` + +### With Custom Secrets + +For services that need custom secrets (like your original example): + +```yaml +jobs: + deploy: + uses: ./.gitea/workflows/cd.yaml + with: + custom_secrets: | + [ + { + "name": "oidc", + "type": "generic", + "data": "{\"clientId\": \"${{ secrets.OIDC_CLIENT_ID }}\", \"clientSecret\": \"${{ secrets.OIDC_CLIENT_SECRET }}\"}" + }, + { + "name": "root-user", + "type": "generic", + "data": "{\"rootUser\": \"${{ secrets.MINIO_ROOT_USER }}\", \"rootPassword\": \"${{ secrets.MINIO_ROOT_PASSWORD }}\"}" + } + ] +``` + +### Advanced Usage with Custom Paths + +If your service uses non-standard directory names: + +```yaml +jobs: + deploy: + uses: ./.gitea/workflows/cd.yaml + with: + k8s_dir: 'kubernetes/' + helmfile_path: 'helm/helmfile.yaml' + custom_secrets: | + [ + { + "name": "database-credentials", + "type": "generic", + "data": "{\"username\": \"${{ secrets.DB_USERNAME }}\", \"password\": \"${{ secrets.DB_PASSWORD }}\"}" + } + ] +``` + +### Deploy from Different Branch + +Deploy from a staging or feature branch: + +```yaml +jobs: + deploy-staging: + uses: ./.gitea/workflows/cd.yaml + with: + deploy_branch: 'staging' + custom_secrets: | + [ + { + "name": "staging-config", + "type": "generic", + "data": "{\"environment\": \"staging\", \"debug\": \"true\"}" + } + ] +``` + +### Force Skip Helm Deployment + +If you want to skip Helm deployment even when helmfile.yaml exists: + +```yaml +jobs: + deploy: + uses: ./.gitea/workflows/cd.yaml + with: + skip_helm_deployment: true + custom_secrets: | + [ + { + "name": "app-config", + "type": "generic", + "data": "{\"apiUrl\": \"${{ secrets.API_URL }}\", \"apiKey\": \"${{ secrets.API_KEY }}\"}" + } + ] +``` + +## Input Parameters + +| Parameter | Description | Default | Required | +|-----------|-------------|---------|----------| +| `k8s_dir` | Path to Kubernetes manifests directory | `k8s/` | No | +| `helmfile_path` | Path to helmfile.yaml | `helmfile.yaml` | No | +| `skip_helm_deployment` | Skip Helm deployment even if helmfile exists | `false` | No | +| `custom_secrets` | JSON array of secrets to create | `[]` | No | +| `deploy_branch` | Branch to deploy from | `main` | No | + +## Custom Secrets Format + +The `custom_secrets` parameter accepts a JSON array where each secret object has: + +```json +{ + "name": "secret-name", + "type": "secret-type", + "data": "{\"key1\": \"value1\", \"key2\": \"value2\"}" +} +``` + +### Secret Object Properties + +- **`name`** (required): The name of the Kubernetes secret +- **`type`** (optional): The type of Kubernetes secret (defaults to "generic") +- **`data`** (required): JSON string containing the secret data + +### Supported Secret Types + +- `generic`: Generic secret (most common) +- `docker-registry`: For Docker registry credentials +- `tls`: For TLS certificates +- `ssh-auth`: For SSH authentication +- `basic-auth`: For basic authentication + +### Example Secret Configurations + +#### OIDC Configuration +```json +{ + "name": "oidc", + "type": "generic", + "data": "{\"clientId\": \"${{ secrets.OIDC_CLIENT_ID }}\", \"clientSecret\": \"${{ secrets.OIDC_CLIENT_SECRET }}\"}" +} +``` + +#### Database Credentials +```json +{ + "name": "database-credentials", + "type": "generic", + "data": "{\"username\": \"${{ secrets.DB_USERNAME }}\", \"password\": \"${{ secrets.DB_PASSWORD }}\"}" +} +``` + +#### Docker Registry +```json +{ + "name": "docker-registry", + "type": "docker-registry", + "data": "{\"username\": \"${{ secrets.DOCKER_USERNAME }}\", \"password\": \"${{ secrets.DOCKER_PASSWORD }}\"}" +} +``` + +## Directory Structure Requirements + +### For Kubernetes-only services: +``` +your-service/ +├── k8s/ +│ ├── deployment.yaml +│ ├── service.yaml +│ └── ... +└── .gitea/workflows/your-workflow.yaml +``` + +### For Helm + Kubernetes services: +``` +your-service/ +├── k8s/ +│ ├── deployment.yaml +│ ├── service.yaml +│ └── ... +├── helmfile.yaml +└── .gitea/workflows/your-workflow.yaml +``` + +## What Gets Deployed + +### Always (if k8s/ directory exists): +- Custom secrets creation (if specified) +- Kubernetes manifest deployment using `kubectl apply` +- Namespace extraction from repository name + +### Conditionally (if helmfile.yaml exists and Helm deployment not skipped): +- Custom secrets creation (if specified) +- Helm chart deployment using `helmfile apply` +- Kubernetes manifests in Helm context + +## Migration from Your Original Workflow + +Your original workflow had these hardcoded secrets: +- `oidc` secret with OIDC_CLIENT_ID and OIDC_CLIENT_SECRET +- `root-user` secret with MINIO_ROOT_USER and MINIO_ROOT_PASSWORD + +To replicate this in the new workflow: + +```yaml +jobs: + deploy: + uses: ./.gitea/workflows/cd.yaml + with: + custom_secrets: | + [ + { + "name": "oidc", + "type": "generic", + "data": "{\"clientId\": \"${{ secrets.OIDC_CLIENT_ID }}\", \"clientSecret\": \"${{ secrets.OIDC_CLIENT_SECRET }}\"}" + }, + { + "name": "root-user", + "type": "generic", + "data": "{\"rootUser\": \"${{ secrets.MINIO_ROOT_USER }}\", \"rootPassword\": \"${{ secrets.MINIO_ROOT_PASSWORD }}\"}" + } + ] +``` + +## Example Workflows + +See `example-cd-usage.yaml` for complete examples of how to use this workflow in different scenarios. + +## Dependencies + +This workflow requires: +- `./.gitea/actions/extract-namespace-from-repo-name` action +- `KUBECONFIG` secret configured in your repository +- Access to your Kubernetes cluster +- `jq` for JSON parsing (included in ubuntu-latest runner) + +## Troubleshooting + +### Helm deployment skipped unexpectedly +- Check if `helmfile.yaml` exists in the expected location +- Verify the `skip_helm_deployment` parameter is not set to `true` +- Ensure the file path is correct if using custom paths + +### Kubernetes deployment skipped +- Verify the `k8s/` directory (or custom path) exists +- Check the directory contains valid Kubernetes manifests + +### Secret creation failures +- Verify the JSON format is valid +- Check that secret names are valid Kubernetes resource names +- Ensure the `data` field contains valid JSON string + +### Permission issues +- Ensure the `KUBECONFIG` secret is properly configured +- Verify the workflow has access to your Kubernetes cluster +- Check that the namespace exists and is accessible + +## Security Considerations + +- **Secret Data**: The `data` field in custom_secrets should contain the actual secret values, not references to GitHub secrets +- **Repository Secrets**: Store sensitive values in GitHub repository secrets and reference them in the workflow +- **Access Control**: Ensure only authorized users can trigger deployments +- **Audit Trail**: All deployments are logged and visible in the GitHub Actions history diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b0eb10 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# Reusable CI Workflow for Kubernetes Services + +This directory contains a reusable CI workflow that automatically detects and validates your Kubernetes services, whether they use Helm + Kubernetes or just Kubernetes manifests. + +## Features + +- **Automatic Detection**: Automatically detects if your service uses Helm (helmfile.yaml) or just Kubernetes manifests +- **Conditional Validation**: Only runs Helm validation when helmfile.yaml exists +- **Flexible Paths**: Configurable paths for k8s directory and helmfile +- **Comprehensive Validation**: Validates both Kubernetes manifests and Helm charts +- **CI Summary**: Provides a clear summary of what was validated + +## Usage + +### Basic Usage (Recommended) + +Simply call the workflow without any parameters - it will automatically detect your service type: + +```yaml +jobs: + ci: + uses: ./.gitea/workflows/ci.yaml +``` + +### Advanced Usage with Custom Paths + +If your service uses non-standard directory names: + +```yaml +jobs: + ci: + uses: ./.gitea/workflows/ci.yaml + with: + k8s_dir: 'kubernetes/' + helmfile_path: 'helm/helmfile.yaml' +``` + +### Force Skip Helm Validation + +If you want to skip Helm validation even when helmfile.yaml exists: + +```yaml +jobs: + ci: + uses: ./.gitea/workflows/ci.yaml + with: + skip_helm_validation: true +``` + +## Input Parameters + +| Parameter | Description | Default | Required | +|-----------|-------------|---------|----------| +| `k8s_dir` | Path to Kubernetes manifests directory | `k8s/` | No | +| `helmfile_path` | Path to helmfile.yaml | `helmfile.yaml` | No | +| `skip_helm_validation` | Skip Helm validation even if helmfile exists | `false` | No | + +## Directory Structure Requirements + +### For Kubernetes-only services: +``` +your-service/ +├── k8s/ +│ ├── deployment.yaml +│ ├── service.yaml +│ └── ... +└── .gitea/workflows/your-workflow.yaml +``` + +### For Helm + Kubernetes services: +``` +your-service/ +├── k8s/ +│ ├── deployment.yaml +│ ├── service.yaml +│ └── ... +├── helmfile.yaml +└── .gitea/workflows/your-workflow.yaml +``` + +## What Gets Validated + +### Always (if k8s/ directory exists): +- Kubernetes manifest validation using `kubectl --dry-run` +- Namespace extraction from repository name +- Basic Kubernetes syntax and schema validation + +### Conditionally (if helmfile.yaml exists and Helm validation not skipped): +- Helm chart validation using `helmfile diff` +- Kubernetes manifests in Helm context +- Helm-specific configurations and values + +## Example Workflows + +See `example-usage.yaml` for complete examples of how to use this workflow in different scenarios. + +## Dependencies + +This workflow requires: +- `./.gitea/actions/extract-namespace-from-repo-name` action +- `KUBECONFIG` secret configured in your repository +- Access to your Kubernetes cluster + +## Troubleshooting + +### Helm validation skipped unexpectedly +- Check if `helmfile.yaml` exists in the expected location +- Verify the `skip_helm_validation` parameter is not set to `true` +- Ensure the file path is correct if using custom paths + +### Kubernetes validation skipped +- Verify the `k8s/` directory (or custom path) exists +- Check the directory contains valid Kubernetes manifests + +### Permission issues +- Ensure the `KUBECONFIG` secret is properly configured +- Verify the workflow has access to your Kubernetes cluster