Recently, I stumbled upon a tool called Taskfile. It is a task runner, similar to Makefile, but with many improvements. I decided to try it and see if it can replace my Makefiles.
Why Taskfile?
First of all, Makefile is a good tool. Today, you can use it on any platform, not just Linux. It is a standard tool for running tasks. It is flexible and powerful. My relation with it is love/hate. It can be tricky to write a good Makefile or to debug it. Reading also can be a challenge. Because of that, when I found out about Taskfile, I decided to try it.
First impressions
Taskfile is written in Go. It is a single binary that you can download and use. If you can run Go programs, you can run Taskfile. It is a big plus for me. See docs on how to install it.
Taskfile is a YAML file. Most of us are familiar with YAML, and many tools use it. It is easy to read, write, and parse, and IDEs have good support. Sometimes, it feels like we are programming in YAML. If you do not believe me, check out your CI/CD pipeline. I bet it is written in YAML.
You can use auto-completion if you use ZSH, Bash, Fish, or PowerShell. It is a big plus for me. I like to have auto-completion for my tools. It makes my life easier, especially when you start adding more and more tasks. When you have a lot of tasks, they can be grouped into namespaces, hidden from a user(internal task), global, or local, etc.
Challenge: automate testing for Klock
Klock is my pet project. It is a ValidatingAdmissionWebhook
for Kubernetes. It enables locking Kubernetes resources. You can read more here Klock - Kubernetes locking.
One of the things that was on my to-do list was to automate testing. I'm doing it locally because it is easier and faster. But still, it is cumbersome to start the Kind cluster, deploy Klock, and run tests. I decided to use Taskfile to automate it.
What I need to do is:
- Start Kind cluster
- Install Cert Manager
- Build K8s manifests
- Build the Docker image if there are any code/configuration changes
- Load the image into a Kind cluster
- Run tests
You can find my initial work on the git branch migrate_taskfile. If you compare it with the main branch, you'll notice I added a new file called Taskfile.yml
. It is a main Taskfile. Also, I added the folder tasks
with two files, Taskfile.testing.yml
and Taskfile.tools.yml
. They are imported into the main Taskfile. The idea is to have a main Taskfile that will import other Taskfiles, while other Taskfiles will contain specific tasks.
Start Kind cluster
First, I need to start the Kind cluster:
version: '3'
vars:
KIND_VERSION: "v0.20.0 go1.20.5"
CERT_MANAGER_VERSION: v1.12.0
IMG: controller:latest
tasks:
init-cluster:
desc: Create a kind cluster named klock
preconditions:
- sh: kind version | grep "{{.KIND_VERSION}}"
msg: "kind version does not match {{.KIND_VERSION}}. Please install right version of kind"
cmds:
- kind create cluster --name klock
status:
- kind get clusters | grep klock
The slug tasks
contain a list of tasks. This task is called init-cluster
. It has a description, preconditions, commands, and status. For any task name
and cmds
slugs are mandatory. Others are optional. What they do:
-
cmds
- list of commands that will be executed -
desc
- description of the task, it will be shown when you run list tasks:task -l
-
preconditions
- list of preconditions that must be met before running the task. If any of the preconditions fail, the task will not be executed. In this case, I check if the Kind version is the same as the one I'm using. If not, the task will fail with the message. -
status
- list of commands that will be run to see if a task can be considered done. That means the task is done if the Kind cluster namedklock
exists. It does not care how a cluster is created. It just checks if it exists.
Notice block vars
. It is a list of variables that can be used in the Taskfile. In this case, I'm using KIND_VERSION
to check if the Kind version is the same as the one I'm using. The Taskfile uses [Go template (https://golang.org/pkg/text/template/) to render variables.
Install Cert Manager
Next, I need to install Cert Manager:
install-cert-manager:
desc: Install cert-manager
deps:
- init-cluster
cmds:
- kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/{{.CERT_MANAGER_VERSION}}/cert-manager.yaml
- echo "Waiting for cert-manager to be ready" && sleep 25
status:
- kubectl -n cert-manager get pods | grep Running | wc -l | grep -q 3
The new thing here is the deps
slug. It is a list of tasks that must be executed before this task. In this case, I need to create a Kind cluster before I can install Cert Manager. Again, I'm using a variable CERT_MANAGER_VERSION
to install the correct version of Cert Manager.
Build K8s manifests
To generate manifests, I'm using kubebuilder tool contrller-gen:
manifests:
desc: Generate manifests e.g. CRD, RBAC etc.
cmds:
- controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
sources:
- main.go
- apis/**/*.go
- pkg/**/*.go
generates:
- config/crd/bases/**/*.yaml
This time I'm using sources
and generates
slugs:
-
sources
is a list of files that will be used to generate manifests -
generates
is a list of files that will be generated
In this case, Taskfile will generate a checksum for sources from the previous run. If the checksum is the same, it will not run the task. It will just print out that the task is done. If the checksum is different, it will run the task. It is a nice feature that can save you some time.
Deploy Klock to Kind cluster
To deploy Klock to the Kind cluster, I'm using kustomize:
deploy:
desc: Deploy the controller to the kind cluster and wait for it to be ready. Use IMG to specify image name
deps:
- install-cert-manager
- manifests
- docker-build
cmds:
- task: kind-load
silent: true
- cd config/manager && kustomize edit set image controller={{.IMG}}
- kustomize build config/default | kubectl apply -f -
- echo "Waiting for controller to be ready" && sleep 25
status:
- kubectl get pods -n klock-system | grep klock-controller | wc -l | grep -q 1
This task depends on install-cert-manager
, manifests
, docker-build
, and kind-load
tasks. It means that those tasks have to be executed before this task.
But a small catch, kind-load
can be done only if docker-build
is done successfully. Other tasks can be executed in parallel. That is why other tasks are in the deps
slug. Tasks in the deps
slug will be executed in parallel. Tasks will be executed in order while in the cmds
slug.
I'm adding a new slug, silent
to suppress the task output.
This way, I managed to do some tasks in parallel and speed up the process.
Run tests
To run tests I'm using KUTTL:
ktest:
desc: Run kuttl tests. Specify image name with IMG
cmds:
- task: deploy
silent: true
- kubectl kuttl test
There is nothing special here. Except I should move the deploy
task to the deps
slug. But this is still a work in progress.
Putting It All Together
My main Taskfile
looks like this:
# https://taskfile.dev
version: '3'
includes:
tools: tasks/Taskfile.tools.yml
tests: tasks/Taskfile.testing.yml
tasks:
default:
silent: true
cmds:
- echo "Welcome to Klock!"
- task -l
I'm including two other Taskfiles
. One is for tools, and the other is for tests. The default
task is to print out a welcome message and list all tasks. Invoking the task
without any arguments, it will run the default
task:
$ task
Welcome to Klock!
task: Available tasks for this project:
* tests:cleanup: Delete the kind cluster named klock
* tests:deploy: Deploy the controller to the kind cluster and wait for it to be ready. Use IMG to specify image name
* tests:docker-build: Build the docker image, specify image name with IMG
* tests:docker-push: Push the docker image, specify image name with IMG
* tests:init-cluster: Create a kind cluster named klock
* tests:install-cert-manager: Install cert-manager
* tests:kind-load: Load the docker image into the kind cluster, specify image name with IMG
* tests:ktest: Run kuttl tests. Specify image name with IMG
* tests:manifests: Generate manifests e.g. CRD, RBAC etc.
* tests:publish: Run tests and publish the docker image. Specify image name with IMG and version with VERSION
* tests:undeploy: Undeploy the controller from the kind cluster
* tools:controller-gen: Download controller-gen locally if necessary.
* tools:create-localbin: Create the localbin directory
* tools:delete-kustomize: Delete kustomize
* tools:envtest: Download envtest-setup locally if necessary.
* tools:install-kustomize: Install kustomize
To run the tests, I'm using task tests:ktest IMG=rnemet/klock:test
. It will run the tests:ktest
task and pass the image name to the task. This image name is propagated to other tasks that need it. As you can see, I grouped tasks into namespaces. I'm using the tests
namespace for tasks related to tests and the tools
namespace for tasks related to tools.
Conclusion
I'm still learning Taskfile. There are better ways to do things. But I'm pleased with the result. Still, I need to migrate other Makefile targets to Taskfile. But the results are promising. Changes are more readable, easier to maintain and to understand. When I migrate the rest of the Makefile targets, I can compare Taskfile and Makefile with more insights. But at the moment, Taskfile is a winner.
Looking into Taskfile Github repo, I can see that the project is active, and much work is underway. I'm looking forward to new features and improvements.
This is a work in progress. I already have ideas on how to make it better. But I will leave that for another blog post. Meanwhile, you can check Taskfile and let me know what you think. Thanks for reading.
Top comments (0)