├── go.mod ├── .gitignore ├── .goreleaser.yml ├── Makefile ├── cmd └── kjob │ ├── main.go │ └── run.go ├── .github └── workflows │ ├── release.yaml │ └── ci.yaml ├── examples ├── curl.yaml └── kubectl.yaml ├── pkg └── jobrunner │ ├── types.go │ └── controller.go ├── README.md ├── LICENSE └── go.sum /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stefanprodan/kjob 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/spf13/cobra v0.0.6 7 | k8s.io/api v0.18.0 8 | k8s.io/apimachinery v0.18.0 9 | k8s.io/client-go v0.18.0 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | bin/ -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - main: ./cmd/kjob 3 | binary: kjob 4 | goos: 5 | - windows 6 | - darwin 7 | - linux 8 | goarch: 9 | - amd64 10 | env: 11 | - CGO_ENABLED=0 12 | archives: 13 | - name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 14 | files: 15 | - none* 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TAG?=latest 2 | VERSION?=$(shell grep 'VERSION' cmd/kjob/main.go | awk '{ print $$4 }' | tr -d '"') 3 | 4 | build: 5 | CGO_ENABLED=0 go build -o ./bin/kjob ./cmd/kjob 6 | 7 | fmt: 8 | gofmt -l -s -w ./ 9 | goimports -l -w ./ 10 | 11 | test-fmt: 12 | gofmt -l -s ./ | grep ".*\.go"; if [ "$$?" = "0" ]; then exit 1; fi 13 | goimports -l ./ | grep ".*\.go"; if [ "$$?" = "0" ]; then exit 1; fi 14 | 15 | test: 16 | go test ./... 17 | 18 | release: 19 | git tag "v$(VERSION)" 20 | git push origin "v$(VERSION)" -------------------------------------------------------------------------------- /cmd/kjob/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var VERSION = "0.4.0" 13 | 14 | var rootCmd = &cobra.Command{ 15 | Use: "kjob", 16 | Short: "Kubernetes job runner", 17 | Version: VERSION, 18 | } 19 | 20 | func main() { 21 | log.SetFlags(0) 22 | 23 | rootCmd.SetArgs(os.Args[1:]) 24 | if err := rootCmd.Execute(); err != nil { 25 | e := err.Error() 26 | fmt.Println(strings.ToUpper(e[:1]) + e[1:]) 27 | os.Exit(1) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Unshallow 15 | run: git fetch --prune --unshallow 16 | - name: Setup Go 17 | uses: actions/setup-go@v2-beta 18 | with: 19 | go-version: 1.14.x 20 | - name: Download release notes 21 | env: 22 | GH_REL_URL: https://github.com/buchanae/github-release-notes/releases/download/0.2.0/github-release-notes-linux-amd64-0.2.0.tar.gz 23 | run: cd /tmp && curl -sSL ${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/ 24 | - name: Generate release notes 25 | run: github-release-notes -org stefanprodan -repo kjob -since-latest-release > /tmp/release.txt 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@v1 28 | with: 29 | version: latest 30 | args: release --release-notes=/tmp/release.txt 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | tags-ignore: 9 | - v.* 10 | 11 | jobs: 12 | e2e: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Restore Cache 18 | uses: actions/cache@v1 19 | with: 20 | path: ~/go/pkg/mod 21 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 22 | restore-keys: | 23 | ${{ runner.os }}-go- 24 | - name: Setup Go 25 | uses: actions/setup-go@v2-beta 26 | with: 27 | go-version: 1.14.x 28 | - name: Test 29 | run: make test 30 | - name: Build 31 | run: make build 32 | - uses: engineerd/setup-kind@v0.3.0 33 | with: 34 | version: "v0.7.0" 35 | image: "kindest/node:v1.18.0" 36 | - name: Integration test (job completed) 37 | run: | 38 | kubectl create ns test 39 | kubectl -n test apply -f examples/curl.yaml 40 | bin/kjob run -t curl -n test -c "echo testing" 41 | - name: Integration test (job failed) 42 | run: | 43 | if bin/kjob run -t curl-fail -n test; then 44 | exit 1 45 | fi 46 | -------------------------------------------------------------------------------- /examples/curl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: curl 5 | spec: 6 | schedule: "*/1 * * * *" 7 | successfulJobsHistoryLimit: 0 8 | failedJobsHistoryLimit: 0 9 | suspend: true 10 | jobTemplate: 11 | spec: 12 | backoffLimit: 0 13 | activeDeadlineSeconds: 100 14 | template: 15 | spec: 16 | restartPolicy: Never 17 | containers: 18 | - name: curl 19 | securityContext: 20 | readOnlyRootFilesystem: true 21 | runAsUser: 10001 22 | image: curlimages/curl:7.69.0 23 | command: 24 | - sh 25 | - -c 26 | - "curl -sL flagger.app/index.yaml | grep generated" 27 | --- 28 | apiVersion: batch/v1beta1 29 | kind: CronJob 30 | metadata: 31 | name: curl-fail 32 | spec: 33 | schedule: "*/1 * * * *" 34 | successfulJobsHistoryLimit: 0 35 | failedJobsHistoryLimit: 0 36 | suspend: true 37 | jobTemplate: 38 | spec: 39 | backoffLimit: 0 40 | activeDeadlineSeconds: 100 41 | template: 42 | spec: 43 | restartPolicy: Never 44 | containers: 45 | - name: curl 46 | securityContext: 47 | readOnlyRootFilesystem: true 48 | runAsUser: 10001 49 | image: curlimages/curl:7.69.0 50 | command: 51 | - sh 52 | - -c 53 | - "echo 'something went wrong' && curl -sL flagger.app | grep make-it-fail" 54 | -------------------------------------------------------------------------------- /examples/kubectl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: test 5 | --- 6 | apiVersion: v1 7 | kind: ServiceAccount 8 | metadata: 9 | name: tester 10 | namespace: test 11 | --- 12 | apiVersion: rbac.authorization.k8s.io/v1 13 | kind: ClusterRoleBinding 14 | metadata: 15 | name: tester 16 | roleRef: 17 | apiGroup: rbac.authorization.k8s.io 18 | kind: ClusterRole 19 | name: cluster-admin 20 | subjects: 21 | - kind: ServiceAccount 22 | name: tester 23 | namespace: test 24 | --- 25 | apiVersion: batch/v1beta1 26 | kind: CronJob 27 | metadata: 28 | name: kubectl 29 | namespace: test 30 | spec: 31 | schedule: "*/1 * * * *" 32 | successfulJobsHistoryLimit: 0 33 | failedJobsHistoryLimit: 0 34 | suspend: true 35 | jobTemplate: 36 | spec: 37 | backoffLimit: 0 38 | activeDeadlineSeconds: 100 39 | template: 40 | spec: 41 | serviceAccountName: tester 42 | restartPolicy: Never 43 | containers: 44 | - name: kubectl 45 | image: bitnami/kubectl:1.17.3 46 | command: 47 | - /bin/sh 48 | - -c 49 | - "kubectl -n $(POD_NAMESPACE) get po" 50 | env: 51 | - name: POD_NAME 52 | valueFrom: 53 | fieldRef: 54 | fieldPath: metadata.name 55 | - name: POD_NAMESPACE 56 | valueFrom: 57 | fieldRef: 58 | fieldPath: metadata.namespace 59 | -------------------------------------------------------------------------------- /pkg/jobrunner/types.go: -------------------------------------------------------------------------------- 1 | package jobrunner 2 | 3 | import "time" 4 | 5 | type Job struct { 6 | // TemplateRef references a Kubernetes object that contains the job template spec. 7 | TemplateRef JobTemplateRef `json:"templateRef"` 8 | 9 | // BackoffLimit specifies the number of retries before marking this job failed. 10 | BackoffLimit int32 `json:"backoffLimit"` 11 | 12 | // Timeout specifies the duration relative to the startTime that the job may be active 13 | // before the system tries to terminate it. 14 | // +optional 15 | Timeout time.Duration `json:"timeout,omitempty"` 16 | 17 | // Command specifies the job container command wrapped in a shell. 18 | // +optional 19 | Command string `json:"command,omitempty"` 20 | 21 | // CommandShell specifies the linux shell that executes the command; defaults to sh. 22 | // +optional 23 | CommandShell string `json:"commandShell,omitempty"` 24 | } 25 | 26 | // JobTemplateRef holds the reference to a Kubernetes object. 27 | type JobTemplateRef struct { 28 | // Name of the Kubernetes object. 29 | Name string `json:"name"` 30 | 31 | // Namespace of the Kubernetes object. 32 | Namespace string `json:"namespace"` 33 | } 34 | 35 | // JobResult describes the result of a Kubernetes job execution. 36 | type JobResult struct { 37 | // Name of the Kubernetes job. 38 | Name string `json:"name"` 39 | 40 | // Namespace of the Kubernetes job. 41 | Namespace string `json:"namespace"` 42 | 43 | // Status describes the completion state of the job. 44 | // +optional 45 | Status *JobStatus `json:"status,omitempty"` 46 | 47 | // Output holds the Kubernetes pod logs collected after job completion. 48 | // +optional 49 | Output string `json:"output,omitempty"` 50 | } 51 | 52 | // JobStatus describes the completion state of a Kubernetes job. 53 | type JobStatus struct { 54 | // Failed means the job has failed its execution. 55 | Failed bool `json:"failed"` 56 | 57 | // Message is a human readable message indicating details about the job execution result. 58 | Message string `json:"message"` 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kjob 2 | 3 | [![e2e](https://github.com/stefanprodan/kjob/workflows/ci/badge.svg)](https://github.com/stefanprodan/kjob/actions) 4 | [![release](https://github.com/stefanprodan/kjob/workflows/release/badge.svg)](https://github.com/stefanprodan/kjob/actions) 5 | 6 | **kjob** is a small utility written in Go that: 7 | * creates a Kubernetes Job from a CronJob template 8 | * overrides the job command if specified 9 | * waits for job completion 10 | * prints the pods logs 11 | * removes the pods and the job object 12 | * exits with status 1 if the job failed 13 | 14 | ## Usage 15 | 16 | Download kjob binary from GitHub [releases](https://github.com/stefanprodan/kjob/releases/latest). 17 | 18 | Create a suspended CronJob that will serve as a template: 19 | 20 | ```bash 21 | cat <