Breaking Down Bundles: A Dive

business man running through water with laptop
After covering Porter history, we’re going to dive into the key functions and code used to actually build a bundle.
This is going to get more in the nitty gritty of actual code execution.
Porter builds Cloud Native Application Bundles (CNAB) — but how does Porter build these beautiful bundles?
Let’s jump into it.
Create
For Porter to create a bundle, it needs to be given a porter.yaml. That porter.yaml defines what the bundle should do, what it needs to run, and what different actions it can take.
Here’s an example of a porter.yaml that just runs a simple script that outputs “Hello World”, “World 2.0” and “Goodbye World”.
schemaType: Bundle\
schemaVersion: 1.0.1\
name: porter-hello\
version: 0.1.0\
description: "An example Porter configuration"\
registry: "localhost:5000"\
mixins:\
- exec\
- docker\
\
install:\
- exec:\
description: "Install Hello World"\
command: ./helpers.sh\
arguments:\
- install\
upgrade:\
- exec:\
description: "World 2.0"\
command: ./helpers.sh\
arguments:\
- upgrade\
uninstall:\
- exec:\
description: "Uninstall Hello World"\
command: ./helpers.sh\
arguments:\
- uninstall\
\
credentials:\
- name: kubeconfig\
path: /home/nonroot/.kube/config\
- name: username\
env: USERNAME\
\
\
parameters:\
- name: mysql_user\
type: string\
default: wordpress
This porter.yaml is just calling a script called ./helper.sh which we can see here:
#!/usr/bin/env bash\
set -euo pipefail\
\
install() {\
echo Hello World\
}\
\
upgrade() {\
echo World 2.0\
}\
\
uninstall() {\
echo Goodbye World\
}\
\
# Call the requested function and pass the arguments as-is\
"$@"
If you’d like to go through getting this running locally, this is part of Porter’s quickstart.
Build
Once your porter.yaml is where you want it, running porter build in the directory of your porter.yaml will kick off the build process.
Porter creates a .cnab directory in the root of each bundle directory for every build, and at the beginning of each build we blow that directory away to start fresh.
An example of the .cnab directory after build:
- porter.yaml\
- cnab/\
- - app/\
- - - mixins/\
- - - - exec/\
- - - - - runtimes/\
- - - - - - exec-runtime (executable) \
- - - - runtimes/\
- - - run (executable)\
- - - porter.yaml\
- - - runtimes/\
- - Dockerfile\
- - bundle.json
Porter builds the porter.yaml in .cnab/app/porter.yaml. This parses Build Options (Insecure Registry, Build Driver, Custom Tags, Lint) and sets the values parsed from user created porter.yaml into the Porter generated porter.yaml — Porter also parses through all the image references defined in the user created porter.yaml and validates them retrieving the image digest for each reference image.
These referenced images are also translated into OCI references, images without a tag specified will be set to latest, and the value of these digests will be set in the “digest” tag in the Porter created porter.yaml
We call ReadManifest() which reads the porter.yaml either from a URL or from a file specified, if neither is defined it defaults to a file specified, which is usually the porter.yamlin that directory you’re currently in. UnmarshalManifest()takes all bytes read from the previous function and places them into their Go struct type so they can be referenced throughout the codebase. Validate() is called which checks to make sure uninstall actions are defined, or handling of different versions of the schema pass through.
buildBundle() will iterate over mixins (think of mixins as the way Porter knows how to talk to other tools) in the manifest and test that each mixin works by getting their directory and then asking them for their versions (similar to just running the executable in the .cnab/mxiins/exec/runtimes/exec-runtimes.exe version). ManifestCoverter()’s job is to take what was read from the above Manifest(YAML) and convert into a structure used by the Bundle (JSON). It instantiates the ManifestCoverter{} type to act as a base for the information. Then ManifestCoverter.ToBundle()takes all the information read from Manifest and stored into variables and assigns that information into structures used by the bundle. This translation from the data from the manifest into the converter struct enables us to be able to write the bundle.json to the cnab/ directory.
(Note: This bundle.json is updated with an up-to-date invocation image digest when we publish it)
GenerateDockerFile() Porter gives users a default Dockerfile (template.Dockerfile) in the root directory when users run porter create. Porter will then take all of the previous information (the manifest, porter configuration, mixins) to compile a DockerFile. This DockerFile will be built and become the invocation image.
GetBuilder() EventuallyPorter will support different build drivers, but right now it is just BuildKit.
Finally, BuildInvocationImage() connects to DockerClient and create a session to a buildx builder — we will establish an SSH connection to this builder and then put all secrets, build arguments, and an unstructured logger into this builder — and then build. This is using DockerClient to stand up a container with all the information we have parsed and formatted in the last steps, and this information in that container.
And now, Publish!
I’m not going as granular into this because it’s a lot of repeated information. Feel encouraged to look at pkg/porter/publish.go for more.
We support PublishfromFile and PublishFromArchive (a zipped file)
(PublishFromFile) Ensure that the bundle version being pushed is the most up to date version. ParseOCIReference for the manifest reference image, as well as the invocation image. Push the invocation image to the OCI registry, and then rewrite what is in cnab/bundle.json with the digest of the pushed invocation image information. PushBundle() pushes the bundle to the reference registry by checking all referenced images in the bundle are present in the repository it is being pushed to, then cnab-to-oci pushes the bundle as an OCI image index manifest
(PublishFromArchive) Using an archived bundle this creates an artifacts/layouts directory and spins up a local go container registry (ggcr) — this leverages cnabio/image-relocation to relocate and rename the image from a public registry, to a local or air gapped registry.