Skip to content

jlsalvador/simple-cicd

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

152 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Simple CI/CD

Simple CI/CD Logo

Manage jobs, workflows, and webhooks natively via K8s APIs.

No dependencies, no virtual machines, no Docker-in-Docker, no need to learn a new language or framework. Just standard K8s Jobs.

Important

Upgrading from a previous version? Check the Migration Guide.


Getting Started

Install via Helm (recommended)

helm upgrade --install \
  --create-namespace \
  --namespace simple-cicd \
  --repo https://jlsalvador.github.io/simple-cicd \
  operator operator

Install via manifest

kubectl apply -f https://github.com/jlsalvador/simple-cicd/releases/latest/download/crds.yaml
kubectl apply -f https://github.com/jlsalvador/simple-cicd/releases/latest/download/operator.yaml

Example

This example creates a workflow that runs a job, echoing the original HTTP request and returning a random exit code. On failure, it triggers a second workflow that echoes "ERROR".

Caution

Job templates must have spec.suspend: true. Without it, Kubernetes will start the Job immediately. The operator automatically sets spec.suspend: false.

# Job that randomly exits with code 0 or 1.
apiVersion: batch/v1
kind: Job
metadata:
  name: job-example-random-exit
  namespace: example
spec:
  suspend: true # Prevents Kubernetes from running this directly.
  backoffLimit: 0
  template:
    spec:
      containers:
        - name: random-exit
          image: bash
          command: ["sh", "-c", "exit $$(($RANDOM % 2))"]
        - name: echo-request
          image: bash
          command:
            - sh
            - -c
            - |
              echo "Host:       $(cat /var/run/secrets/kubernetes.io/request/host)"
              echo "Headers:    $(cat /var/run/secrets/kubernetes.io/request/headers)"
              echo "Method:     $(cat /var/run/secrets/kubernetes.io/request/method)"
              echo "URL:        $(cat /var/run/secrets/kubernetes.io/request/url)"
              echo "RemoteAddr: $(cat /var/run/secrets/kubernetes.io/request/remoteAddr)"
              echo "Timestamp:  $(cat /var/run/secrets/kubernetes.io/request/timestamp)"
              echo "Body:       $(cat /var/run/secrets/kubernetes.io/request/body)"
      restartPolicy: Never # Do not re-run the pod if something fails.
---
# Job that echoes "ERROR".
apiVersion: batch/v1
kind: Job
metadata:
  name: job-example-error
  namespace: example
spec:
  suspend: true
  template:
    spec:
      containers:
        - name: error
          image: bash
          command: ["echo", "ERROR"]
      restartPolicy: Never
---
# Workflow triggered on failure: runs job-example-error.
apiVersion: simple-cicd.jlsalvador.online/v1alpha2
kind: Workflow
metadata:
  name: workflow-example-on-failure
  namespace: example
spec:
  jobsToBeCloned:
    - name: job-example-error
---
# Main workflow: runs job-example-random-exit, then workflow-example-on-failure on any failure.
apiVersion: simple-cicd.jlsalvador.online/v1alpha2
kind: Workflow
metadata:
  name: workflow-example
  namespace: example
spec:
  jobsToBeCloned:
    - name: job-example-random-exit
  next:
    - name: workflow-example-on-failure
      when: OnAnyFailure
---
# WorkflowWebhook: listens on /example/workflowwebhook-example.
apiVersion: simple-cicd.jlsalvador.online/v1alpha2
kind: WorkflowWebhook
metadata:
  name: workflowwebhook-example
  namespace: example
spec:
  workflows:
    - name: workflow-example
  ttlSecondsAfterFinished: 3600 # Clean up WWRs after 1 hour.

Trigger the WorkflowWebhook via kubectl port-forward:

kubectl -n simple-cicd port-forward svc/operator 9000:9000 &
curl -XPOST http://localhost:9000/example/workflowwebhook-example

Tip

You can also reach the operator directly from within the cluster or expose it through any Service. Remember to set up proper authentication & authorization.


Request Data in Jobs

Every Job cloned by the operator has the original HTTP request data mounted as read-only files at /var/run/secrets/kubernetes.io/request/ inside all its containers.

File Content
body Request body
headers All headers serialised as JSON
host Host header value
method HTTP method (GET, POST, …)
url Full request URL
remoteAddr Client IP and port (e.g. 10.0.0.5:54321)
timestamp Time the request was received (UNIX timestamp)

How It Works

Simple CI/CD follows the Kubernetes Operator pattern.

The operator exposes an HTTP server on port 9000. Each incoming request to /{namespace}/{workflowWebhookName} creates a WorkflowWebhookRequest and starts the reconciliation loop.

flowchart TD
    %% Style Definitions
    classDef trigger fill:#e3f2fd,stroke:#1e88e5,stroke-width:2px,color:#000
    classDef k8s fill:#e8f5e9,stroke:#43a047,stroke-width:2px,color:#000
    classDef secret fill:#fff3e0,stroke:#fb8c00,stroke-width:2px,color:#000
    classDef decision fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,color:#000
    classDef endState fill:#ffebee,stroke:#e53935,stroke-width:2px,color:#000
    classDef success fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000
    classDef failure fill:#ffccbc,stroke:#d84315,stroke-width:2px,color:#000

    A["🌐 HTTP POST<br>/{namespace}/{webhookName}"]:::trigger

    subgraph Phase1 ["πŸ“₯ Request"]
        B["πŸ“„ WorkflowWebhookRequest"]:::k8s
    end

    subgraph Phase2 ["βš™οΈ Execution"]
        D["πŸ”— WorkflowWebhook"]:::k8s
        E["πŸƒ Workflow"]:::k8s
        C[("πŸ” Request Secret")]:::secret
        F["πŸ—οΈ Jobs"]:::k8s
    end

    subgraph Phase3 ["🚦 Evaluation & Control"]
        G{"πŸ‘οΈ watch Job status"}:::decision
        H["βœ… next Workflow(s)<br>when: OnSuccess / Always"]:::success
        I["❌ next Workflow(s)<br>when: OnFailure / Always"]:::failure
        J{"πŸ” more Workflows?"}:::decision
    end

    K(["🏁 WorkflowWebhookRequest done"]):::endState

    %% Connections
    A --> B
    A --> C

    B -->|reads| D
    D --> E

    %% Dependency Injection: Secret mounted into Pods
    C -.->|mounted into every Job pod| F

    E -->|clone| F
    F --> G

    G -->|success| H
    G -->|failure| I

    H --> J
    I --> J

    %% Loops and Termination
    J -->|no| K
    J -->|yes| E
Loading

Reconciliation loop

  1. A webhook HTTP request arrives. The operator creates a Secret containing the request data and a WorkflowWebhookRequest (WWR) referencing it.
  2. The reconciler reads the WWR, resolves the referenced WorkflowWebhook and its Workflow list.
  3. For each active Workflow, referenced Jobs are cloned. Jobs in the same namespace as the WWR mount the request Secret directly. Jobs in other namespaces receive a per-job mirrored copy of the Secret, owned by the Job and automatically garbage-collected when the Job is deleted.
  4. The reconciler polls cloned Jobs until all finish, then evaluates when conditions to determine which next Workflows to trigger.
  5. Steps 3-4 repeat until no further Workflows remain, at which point the WWR is marked done: true.

Custom Resources

Workflow

Defines which Jobs to clone and which Workflows to trigger next based on exit status.

apiVersion: simple-cicd.jlsalvador.online/v1alpha2
kind: Workflow
metadata:
  name: my-workflow
  namespace: example
spec:
  jobsToBeCloned:
    - name: my-job
      namespace: example # Defaults to the Workflow's own namespace when omitted.
  next:
    - name: my-next-workflow
      when: OnAnyFailure # OnSuccess | OnAnySuccess | OnFailure | OnAnyFailure | Always
  suspend: false # Set to true to skip execution without deleting.

WorkflowWebhook

Binds an HTTP path to one or more Workflows and controls concurrency and lifecycle policies. All policy fields are copied into each WorkflowWebhookRequest at creation time and are not affected by later changes to the WorkflowWebhook.

apiVersion: simple-cicd.jlsalvador.online/v1alpha2
kind: WorkflowWebhook
metadata:
  name: my-webhook
  namespace: example
spec:
  workflows:
    - name: my-workflow
      namespace: example   # Defaults to the WorkflowWebhook's own namespace when omitted.
  concurrencyPolicy: Allow # Allow | Forbid | Replace
  suspend: false
  ttlSecondsAfterFinished: 3600 # Delete the WWR 1 hour after it finishes. Omit to keep it forever.
  activeDeadlineSeconds: 1800   # Mark the WWR done after 30 min if still running. Omit to disable.

Concurrency policies

Policy Behaviour
Allow Multiple WWRs can run simultaneously (default).
Forbid Creates the WWR without executing any Workflow.
Replace Deletes any running WWR and starts a fresh one.

Note

With Forbid, the HTTP response is still 202 Accepted. The WWR is created and immediately marked done. Inspect status.conditions to determine whether execution was actually skipped.

when conditions

Value Trigger condition
OnSuccess All Jobs in the step succeeded (default)
OnAnySuccess At least one Job succeeded
OnFailure All Jobs in the step failed
OnAnyFailure At least one Job failed
Always Always trigger regardless of outcome

ttlSecondsAfterFinished

When set, the operator automatically deletes the WWR the specified number of seconds after it completes. 0 means delete immediately on completion. Omitting the field keeps the WWR indefinitely.

activeDeadlineSeconds

When set, the operator forcibly terminates all running Jobs and marks the WWR done with reason DeadlineExceeded if it has not completed within the specified number of seconds of its creation. Omitting the field disables the deadline.

Note

activeDeadlineSeconds is measured from metadata.creationTimestamp, not from when execution actually started, making it a hard wall-clock limit on the total time a request may occupy the system.

WorkflowWebhookRequest

Created automatically by the operator on each incoming HTTP request. Tracks the full execution lifecycle.

kubectl get wwr -n example

NAME                            DONE   WEBHOOK                   SUCCESSFUL JOBS   FAILED JOBS   AGE
workflowwebhook-example-b5lmh   true   workflowwebhook-example   1                 0             76s
workflowwebhook-example-lw2ws   true   workflowwebhook-example   1                 1             12m

Status conditions

The status.conditions field records lifecycle events. The most recent condition reflects the final outcome:

reason Meaning
Done All Workflows completed normally.
Suspended No Workflows were executed.
Forbidden Another WWR was running.
NoWorkflows Nothing to execute.
DeadlineExceeded Running Jobs were terminated.
kubectl get wwr -n example -o jsonpath='{.items[-1].status.conditions[-1]}'

Motivation

"Simple CI/CD doesn't try to compete with GitHub Actions in features; it competes in simplicity and low overhead."

Existing solutions either impose excessive requirements or require virtual machines, Docker-in-Docker, or components outside the Kubernetes ecosystem.

Simple CI/CD uses native Kubernetes Jobs, so you do not need to learn a new language.

Disclaimer: based on personal opinion. Please do your own research.
Alternative Advantages Disadvantages
GitHub Actions
  • De-facto standard for public repositories on GitHub.
  • Controlled by Microsoft.
  • Closed garden.
  • Actions must be published publicly.
  • Limited by a paywall.
  • Not suitable for air-gap environments or private clusters.
Jenkins
  • Open-source.
  • Mature and battle-tested.
  • Backed by a broad community.
  • Resource hungry.
  • Requires Groovy for advanced pipelines.
  • Needs add-ons for Kubernetes integration.
  • Limited CLI support.
Tekton
  • Cannot use more than one PersistentVolume per Pod.
  • Frequent API churn leading to deprecated Catalog tasks.
Drone
  • Open-source.
  • Controlled by Harness.
  • Community pull requests receive limited attention.
Woodpecker CI
  • Open-source community fork of Drone.
  • Community supported.
  • No namespaced PersistentVolume support across tasks.
  • Limited Kubernetes support.
Gitea act_runner
  • Community effort for on-premise GitHub Actions compatibility.
  • Requires a VM-like environment, as GitHub Actions does.
  • No native Kubernetes integration.
Simple CI/CD
  • Open-source.
  • Kubernetes-native operator.
  • Platform agnostic (cloud, on-premise, hybrid, air-gap).
  • No external dependencies.
  • Low resource usage (~8 MB RAM).
  • Experimental; not yet battle-tested in production.
  • Maintained by a single individual.
  • No web UI (yet).

License

Copyright 2023 JosΓ© Luis Salvador Rufo salvador.joseluis@gmail.com.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.