Examples

Disclaimer

Writing your first Flight

The most simple implementation of a Flight, is to simply write the resources you want to standard out:

// example.go

package main

import "fmt"

var deployment = `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: example-app
  labels:
    app: example-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: example-app
  template:
    metadata:
      labels:
        app: example-app
    spec:
      containers:
      - name: example-app-container
        image: nginx:latest  # Replace with your actual container image
        ports:
        - containerPort: 80
`

func main() {
	fmt.Println(deployment)
}

Compile it:

GOOS=wasip1 GOARCH=wasm go build -o example.wasm ./example.go

Deploy it:

yoke takeoff example ./example.wasm

And we're done! With this example, you have defined a valid Flight, compiled it to wasm, and had yoke deploy the first revision of a release named example.

However, although illustrative as a first example, we haven't gained any of the advantages of using code to describe our k8s packages. Indeed the previous example is equivalent to feeding the raw text directly to yoke:

yoke takeoff example < example.yaml

Using Client-Go to build Flights

Now that we understand that Flights are simply programs that output the resources as JSON/YAML, we can start representing our resources as Go values utilizing our own types and abstractions, or those provided for us by other libraries.

As it happens client-go has a package named applyconfigurations that allows us to build representations of core K8s resources. Let's use it to redefine our deployment from the earlier example, and to add a service for it.

// example.go
package main

import (
	"encoding/json"
	"os"

	"k8s.io/apimachinery/pkg/util/intstr"

	apicore "k8s.io/api/core/v1"
	appsv1 "k8s.io/client-go/applyconfigurations/apps/v1"
	corev1 "k8s.io/client-go/applyconfigurations/core/v1"
	metav1 "k8s.io/client-go/applyconfigurations/meta/v1"
)

func main() {
	appName := "example-app"

	appLabels := map[string]string{"app": appName}

	deployment := appsv1.Deployment(appName, "").
		WithLabels(appLabels).
		WithSpec(
			appsv1.DeploymentSpec().
				WithReplicas(2).
				WithSelector(
					metav1.LabelSelector().WithMatchLabels(appLabels),
				).
				WithTemplate(
					corev1.PodTemplateSpec().
						WithLabels(appLabels).
						WithSpec(
							corev1.PodSpec().
								WithContainers(
									corev1.Container().
										WithName(appName).
										WithImage("nginx:latest").
										WithPorts(corev1.ContainerPort().WithContainerPort(80)),
								),
						),
				),
		)

	service := corev1.Service(appName, "").
		WithLabels(appLabels).
		WithSpec(
			corev1.ServiceSpec().
				WithSelector(appLabels).
				WithType(apicore.ServiceTypeClusterIP).
				WithPorts(
					corev1.ServicePort().
						WithProtocol(apicore.ProtocolTCP).
						WithPort(80).
						WithTargetPort(intstr.FromInt(80)),
				),
		)

	json.NewEncoder(os.Stdout).Encode([]any{deployment, service})
}

Repeating the steps as in the first example, we:

Compile it:

GOOS=wasip1 GOARCH=wasm go build -o example.wasm ./example.go

Deploy it:

yoke takeoff example ./example.wasm

Take-aways

We have created a second revision of our release called example. It contains both a deployment and a service. Furthermore, it comes with all the advantages we expect from code. It has type-safety, IntelliSense, and documentation is a click away. It is easy to imaging how we may break things out as a reusable functions, create our own contracts, and write tests.

In this example, we used the client-go applyconfigurations package to build our resource values. However you may use the types resource types directly, or create your own simplified representations.

Configuring your Flight

In the previous examples, our Flights have been static. Every invokation will return the exact same output. This is a valid approach, especially when you are reusing low-altitude Flight functions and want to take advantage of type-safety and features provided by your coding environment. Changes can be applied by re-compiling different configurations of the Flight and versioning those assets through time.

However, sometimes we wish to have one compiled program that can be invoked at deploy time with different settings. To achieve this, Yoke supports instantiating wasm executables with both flags and standard input.

Let's look at a simplified version of the deployment example from above and let us consider how we could configure the replica count.

// example.go

package main

import (
	"encoding/json"
	"os"

	appsv1 "k8s.io/client-go/applyconfigurations/apps/v1"
)

// This example is not fully runnable
// As the deployment is missing too many properties to be useful.
// We will only be interested in learning to configure the Replicas value.
func main() {
	deployment := appsv1.Deployment("example-app", "").
		WithSpec(appsv1.DeploymentSpec().WithReplicas(2))

	json.NewEncoder(os.Stdout).Encode(deployment)
}

Using Flags

// example.go

package main

import (
	"encoding/json"
	"flag"
	"os"

	appsv1 "k8s.io/client-go/applyconfigurations/apps/v1"
)

func main() {
	replicas := flag.Int("replicas", 2, "replica count for deployment")
	flag.Parse()

	deployment := appsv1.Deployment("example-app", "").
		WithSpec(appsv1.DeploymentSpec().WithReplicas(int32(*replicas)))

	json.NewEncoder(os.Stdout).Encode(deployment)
}

After compiling it we can invoke it from the Yoke CLI with flags:

yoke takeoff example ./example.wasm -- --replicas=5

Using Stdin

This approach is most similar to how Helm uses `values.yaml` to pass configuration, as using stdin is equivalent to passing a file.

// example.go

package main

import (
	"encoding/json"
	"io"
	"os"

	appsv1 "k8s.io/client-go/applyconfigurations/apps/v1"
)

type Values struct {
	Replicas int32 `json:"replicas"`
}

func main() {
	// Default Values
	values := Values{Replicas: 2}

	// Here we need to check that the error is not io.EOF as this is what you will get if
	// no standard input is used.
	if err := json.NewDecoder(os.Stdin).Decode(&values); err != nil && err != io.EOF {
		panic(err)
	}

	deployment := appsv1.Deployment("example-app", "").
		WithSpec(appsv1.DeploymentSpec().WithReplicas(values.Replicas))

	json.NewEncoder(os.Stdout).Encode(deployment)
}

After compiling it we can invoke it from the Yoke CLI with stdin:

yoke takeoff example ./example.wasm <<< '{"replicas":5}'

or

yoke takeoff example ./example.wasm < values.json