344 lines
15 KiB
Markdown
344 lines
15 KiB
Markdown
---
|
|
type: "blog-post"
|
|
title: "Streamlining Tooling Management: The Idea Behind `Cuddle`"
|
|
description: "In this post I go over `cuddle`, which is an advanced code-sharing tool built so solve maintainability issues around templating, scaffolding and sharing of project specific tools"
|
|
draft: false
|
|
date: "2023-07-28"
|
|
updates:
|
|
- time: "2023-07-28"
|
|
description: "first iteration"
|
|
- time: "2023-07-28"
|
|
description: "Fixed some typos"
|
|
tags:
|
|
- '#blog'
|
|
---
|
|
|
|
In a
|
|
[previous post](https://blog.kasperhermansen.com/posts/development-stack-2023/)
|
|
I explained that I've got my own internal cloud platform I am hosting for my own
|
|
applications. In this post I'd like to drill down into a foundational tool I am
|
|
using to manage the code both running the cloud, but also the applications
|
|
underneath.
|
|
|
|
First a small story to explain some of the motivation: When I started out
|
|
programming I like many others didn't know about git, or other vcs, so I just
|
|
used files and folders in brute force manner, when I had to share some code I
|
|
just packed it up in a zip file and yeeted it wherever it needed to go.
|
|
|
|
That was fine for a while, but eventually I found git and began using GitHub
|
|
Desktop, which probably still is the cleanest option for getting someone up and
|
|
running with git, even if it can become a shackle quite quickly, as you really
|
|
need to know git to use git.
|
|
|
|
However, now armed with git, github and so on. I began to have a problem that
|
|
kind of like the zip situation, everytime I started a new project I basically
|
|
copied an old project, removed the cruft that I didn't need and then simply
|
|
began working anew. A process filled with `rm -rf`, `sed` and more.
|
|
|
|
I also didn't like the mono repository approach, my reasons include that I
|
|
wasn't as proficient in building CI systems, and that my projects are so diverse
|
|
that they don't need to be in lock step. I would also like to share some stuff
|
|
publically, which complicates the whole mono repo approach.
|
|
|
|
A mono repository is a great way to remove some of the repetition, but you pay
|
|
for it in terms of continuous integration complexity, as such you need a much
|
|
more sophisticated building and testing solution than a regular multi-repository
|
|
approach, where each significant component has its own repo.
|
|
|
|
Anyways, I tried to solve the issue with various templating tools, but that
|
|
simply led to another problem, templating is great at scaffolding, but horrible
|
|
at maintenance. You get drift for various parts of the system.
|
|
|
|
My repositories usually consist of a few layers:
|
|
|
|
1. Project setup
|
|
2. CI definitions
|
|
3. Code
|
|
4. Various artifacts and docs
|
|
|
|
Most of these can be scaffolded, but in later projects I may change my mind and
|
|
add some stuff to CI, a new way of handling environment variables, or changing
|
|
the general infrastructure code.
|
|
|
|
And will I go back and update 20 other repositories manually with the same
|
|
changes? no.
|
|
|
|
There are some solutions to this problem, specifically code-doctoring, or
|
|
modding. However, that is hitting a nail with a sledge hammer, or rather a
|
|
screw. It is simply an incompatible problem, with some overlap. (it is primarily
|
|
built for handling small breaking changes in large open source projects, nextjs
|
|
have used this approach in the past, and is fairly common in the web scene).
|
|
|
|
## Shuttle
|
|
|
|
At work we've got an open source tool called
|
|
[lunarway/shuttle](https://github.com/lunarway/shuttle). It is basically a
|
|
golang cli application, which allows a repository to link to another for various
|
|
parts shown above.
|
|
|
|
It can link (not as in ln, but on a more implicit basis)
|
|
|
|
1. Project setup such as .env files, docker-compose setups, various commands
|
|
etc. Such as spinning up a dev environment etc.
|
|
2. It can contain a skeleton of a CI system. such that a project will only need
|
|
either a small bootstrap ci, basically just telling it to use shuttle to
|
|
handle it, or nothing if using Jenkins (which we do a work).
|
|
3. Various artifacts and docs (setup)
|
|
|
|
This is extremely nice, as we remove a lot of the boilerplate from our projects,
|
|
so that we can focus on what is important the code. This tool as we've found out
|
|
kind of gives the same benefit as having a monorepository, though with a
|
|
staggered update cycle.
|
|
|
|
It is run like so:
|
|
|
|
```
|
|
shuttle run build
|
|
shuttle run test
|
|
shuttle run *
|
|
```
|
|
|
|
Each of these commands will trigger some shell scripts either in the local
|
|
repository or in the parent plan.
|
|
|
|
The same shuttle is used in CI, to kick of various steps, again, such as
|
|
`build`, `test`, `generate-k8s-config` etc.
|
|
|
|
A shuttle spec at its most basic is just a file pointing at a parent plan, along
|
|
with some variables to be used in templating or discovery purposes.
|
|
|
|
```yaml
|
|
plan: "git@github.com/lunarway/shuttle-plan-example"
|
|
vars:
|
|
name: my-plan
|
|
squad: some-squad
|
|
```
|
|
|
|
A parent plan looks the same but is called `plan.yaml` instead of `shuttle.yaml`
|
|
|
|
scripts can also be defined in either the plan or shuttle files
|
|
|
|
```yaml
|
|
...
|
|
scripts:
|
|
build:
|
|
actions:
|
|
- shell: go build main.go
|
|
- shell: $scripts/build.sh
|
|
```
|
|
|
|
You are free to choose whatever floats your boat. I've also added native golang
|
|
actions, which doesn't require this setup, but that isn't relevant for this
|
|
post.
|
|
|
|
This is a very useful tool, and I could go and just use that. But I like to
|
|
tinker with my own things, so I've built my own to expand on its capabilities,
|
|
some of which I would need buy in from, in the company, which I am not
|
|
interested in for personal projects.
|
|
|
|
Shuttle itself is also a fairly simple tool, as what is important is what it
|
|
provides, not the tool itself.
|
|
|
|
## Cuddle
|
|
|
|
As such I've built a tool called `cuddle`, which is a CLI written in rust. My
|
|
vision for `cuddle` is that it can support the same features, but on a wider
|
|
spectrum, as well as making people able to go one step further.
|
|
|
|
It runs in nearly the exact way as above
|
|
|
|
One of the problems with shuttle, is that it heavily implies that commands
|
|
should be written in `shell`, this is great for hacks, and small tools, but not
|
|
great for delivering a product. I actually solved this for shuttle allowing it
|
|
to call natively into golang without having to write a line of shell script.
|
|
[lunarway/shuttle#159](https://github.com/lunarway/shuttle/commit/1a57a736391d9e500bdf0dce9467627e0eb9430c)
|
|
it works pretty well, if I have to say so myself, and if you don't have golang
|
|
installed, it will use docker in the background to build the plugins needed for
|
|
the commands to be executable.
|
|
|
|
I want some of the same features myself. I've already gotten `rhai` and `lua` to
|
|
work in cuddle, but I want something more. I want to use `rust` and I want it to
|
|
be a bigger focus in the tooling allowing for greater expandability and
|
|
pluggability.
|
|
|
|
### Code sharing
|
|
|
|
Right now shuttle always has this structure
|
|
|
|
```
|
|
shuttle service -> shuttle plan
|
|
```
|
|
|
|
This means that a repository can inherit stuff from just a single plan, which
|
|
can then include the pipeline and what not. But the plan itself cannot inherit
|
|
from more plans, in turn allowing a deep dependency chain. A shuttle plan can
|
|
act like a shuttle service inheriting from another plan, but that way it won't
|
|
allow it to distribute the base plans files.
|
|
|
|
I already have solved this for `cuddle`, such that we can have a deep as we want
|
|
dependency chain. However, I would like to flip this on its head a bit. See my
|
|
post of
|
|
[distributing continuous integration](https://blog.kasperhermansen.com/posts/distributing-continuous-integration/).
|
|
|
|
Cuddle right now has a dependency graph like so
|
|
|
|
```
|
|
cuddle service -> cuddle plan -> cuddle plan ->*
|
|
```
|
|
|
|
This basically means that cuddle can have infinite plans (or as deep as the
|
|
nesting in file systems allow), however only one at a time However I'd like to
|
|
split this out into more well defined components.
|
|
|
|
## Cuddle components
|
|
|
|
Kind of like a more traditional software development flow.
|
|
|
|
Such as:
|
|
|
|
```
|
|
cuddle service ->* cuddle component
|
|
-> cuddle plan -> cuddle plan ->*
|
|
->* cuddle *component
|
|
```
|
|
|
|
A cuddle component is technically a hybrid between a library and plugin. It
|
|
builds like a library, but functions as a plugin. That is because it should be
|
|
cross platform executable like a step in a CI platform is, but provide a more
|
|
fine grained features and api. Such as a cli script, but should either execute
|
|
as a docker run, a webassembly function, or one of the built in scripting
|
|
languages. A compiled language is typically a nogo, it is simply too slow for
|
|
immediate execution. Unless you use golang, because it is typically fast enough
|
|
for this usecase.
|
|
|
|
Now you may well have a good question, why not just use a regular package
|
|
manager and execution environment like: rust/cargo or ts/deno or another
|
|
language of choice.
|
|
|
|
### Cuddle constraints
|
|
|
|
There are a few reasons, to show them I will first have to highlight why this is
|
|
different than regular software development:
|
|
|
|
Cuddle is a traditional cli, as such it needs to uphold a few guarantees.
|
|
|
|
1. Firstly `cuddle` as a tool needs to be _fast_, fast enough that you don't
|
|
notice that it runs a lot of stuff underneath.
|
|
2. It needs to provide a good developer experience. `cuddle` provides its tools
|
|
as a product, as such we need a good experience using said products.
|
|
3. `cuddle` calls needs to be compose-able, such that you can pipe `cuddle`
|
|
output into regular unix or windows tools, depending on your needs.
|
|
4. `cuddle` services should not require maintenance to be up to date. Unless the
|
|
developers choose to using some of the various escape hatches.
|
|
|
|
Also I see `cuddle` as an enabler. This means that workflows should be built
|
|
around it. You may want to script the usage of `cuddle` runs yourself. This
|
|
should only be for the individual. If a squad needs a curated list of tools,
|
|
they can simply maintain either their own component or plan and inherit from
|
|
that.
|
|
|
|
For example I've built a tmux workflow around it, which opens a new tap, splits
|
|
the window into multiple panes, giving me an auto runner for tests, as well as
|
|
the binary (so I can access the webserver), a shell, and access to a test or
|
|
local database for debugging purposes.
|
|
|
|
This is highly opinionated towards me, and won't in its present form be useful
|
|
for others.
|
|
|
|
### Releasing plans and components
|
|
|
|
As such a traditional package manager won't work. This is mainly because package
|
|
managers rely on versioning and lock files to maintain a consistent set of
|
|
libraries to use. This is pretty good for a tools need. But not great if we
|
|
don't want to offload that burden on developers. If we choose that approach, we
|
|
would have a few problems.
|
|
|
|
1. Each time a cuddle or one of it dependent components were updated, we would
|
|
need to release a new semantic version, which would require the developers to
|
|
update. This may be moving quite fast, as such it is nearly a full-time job
|
|
for developers with big portfolios to maintain said dependencies.
|
|
2. Another as we've done in lunar is simply pulling a fresh plan every time.
|
|
This makes sure we're always up to date, or at least as long as the projects
|
|
are actually run and released. Here we allow various escape hatches, for
|
|
setting static commits, branches, tags what have you.
|
|
|
|
Without sacrificing too much developer experience on the publishing side, we
|
|
need to come up with a good approach for decoupling development from releasing.
|
|
Like traditional software.
|
|
|
|
In this case, the plugins and services will internally use semver, for signaling
|
|
breaking changes. This is useful for showing diffs and what now to developers
|
|
using the tool.
|
|
|
|
However, when we release stuff, releasing it on a channel instead allows a great
|
|
deal of benefit, first. We can choose which appetite you want your service to
|
|
run on. You may choose to use, either pre-release, or default (stable).
|
|
|
|
pre-release allows me to dog-food the plans, during testing, without breaking
|
|
all my tools, and services. Stable which is default, will as mention provide a
|
|
more thoroughly reviewed change set.
|
|
|
|
It is required to have a semver release, to release to a channel. This is for a
|
|
few reasons, but mostly for providing release artifacts. The services shouldn't
|
|
need to build anything themselves. This is to maintain speed, and usability.
|
|
|
|
Each component will simply function like regular libraries, releasing software
|
|
as normal.
|
|
|
|
Each plan will curate a set of components to release, and will handle them like
|
|
normal software releases, i.e. version and lock files and all that jazz. For
|
|
each release it will receive pull-requests with updated dependencies provided by
|
|
`renovate`.
|
|
|
|
This allows each plan to curate an experience for developers. A backend engineer
|
|
will not have the same needs, as a frontend engineer, or a db or an SRE etc.
|
|
|
|
However, this should provide a sufficiently sophisticated dependency chain that
|
|
stuff can actually be built with it, that is maintainable, and stable enough.
|
|
|
|
## Plans as binaries
|
|
|
|
This means that each plan on release can be turned into binaries, either regular
|
|
elf binaries, or wasm. I haven't decided yet, but wasm may have too many
|
|
constraints to be viable.
|
|
|
|
When cuddle runs for the first time in a service, will simply look at the
|
|
binary, its self reported included files, such as a cuddle spec, and other
|
|
included files, it will then form the dependency graph as it goes, downloading
|
|
all plans as it navigates the chain.
|
|
|
|
This is done serially for now, as it would require a registry to form these
|
|
graph relationships, which isn't needed right now, while the projects are small.
|
|
|
|
A `cuddle` service can also contain components, however, those will be built
|
|
adhoc, and function like a normal software project, no way to get around that
|
|
other than surfacing the components as binaries as well, which may become a tad
|
|
bit complicated to manage.
|
|
|
|
## Options for not breaking git history.
|
|
|
|
Right now the `cuddle` services rely on an external project to function, this
|
|
makes history non viable out of the box, because it implies that _everything_ in
|
|
the service has to be forward compatible. For example would `git bisect` be able
|
|
to run on a 3yr old `cuddle plan`, including changes to `cuddle` itself.
|
|
Probably not, and it doesn't fit the spirit of bisect, as you wouldn't get the
|
|
same binaries.
|
|
|
|
Instead, what should be done, is that cuddle will detect if running under a
|
|
bisect or some such, I haven't figured out entirely how to do this yet. And then
|
|
pick a release from a release date, that is older than the commit itself.
|
|
|
|
This should get as close as we can to getting reproducible builds, though it is
|
|
definitely a downside, so if this is a deal breaker then `cuddle` or `shuttle`
|
|
for that matter isn't for you. It isn't something I did myself that often, so it
|
|
isn't for me. It sadly is mostly one of those tools you don't need, until you
|
|
**really** need it.
|
|
|
|
# Conclusion
|
|
|
|
In this post I've gone over my own home built `cuddle` code sharing tool,
|
|
explained why it is useful, and what is wrong with current workflows in
|
|
multi-repository organisations. it is a bad bit more complicated than it needs
|
|
to be, but it provides a useful way of exploring new usecases and removing
|
|
pain-points I am currently experiencing.
|