Skip to content

Getting started

Disclaimer

  • the following examples are written in Go but any language that compiles to wasm is a first class citizen for Yoke. Writing it in Go allows us to take advantage of Kubernete’s core api packages. Languages that do not compile to wasm can still be used but not benefit from Yoke’s asset management. For more information see Why Wasm.
  • These examples assume some familiarity with the Go toolchain, and that yoke has already been installed.

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:

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

Deploy it:

Terminal window
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:

Terminal window
yoke takeoff example < example.yaml

Using Typed Code

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 k8s.io/api is the canonical location of the Kubernetes API definition. Let’s use it to redefine our deployment from the earlier example, and to add a service for it. All this whilst maintaining declarative yet strongly typed code.

The previous example was as unidiomatic as possible. This time we can breakdown the construction of resources into their own functions and create any signature or contract that we want.

example.go
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/yokecd/yoke/pkg/flight"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/ptr"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
var (
release = flight.Release() // the first argument passed to yoke takeoff; ie: yoke takeoff RELEASE foo
namespace = flight.Namespace() // the value of the flag namespace during takeoff; ie: yoke takeoff -namespace NAMESPACE ...
labels = map[string]string{"app": release}
)
resources := []flight.Resource{
CreateDeployment(DeploymentConfig{
Name: release,
Namespace: namespace,
Labels: labels,
Replicas: 2,
}),
CreateService(ServiceConfig{
Name: release,
Namespace: namespace,
Labels: labels,
Port: 80,
TargetPort: 80,
}),
}
return json.NewEncoder(os.Stdout).Encode(resources)
}
type DeploymentConfig struct {
Name string
Namespace string
Labels map[string]string
Replicas int32
}
func CreateDeployment(cfg DeploymentConfig) *appsv1.Deployment {
return &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{
APIVersion: appsv1.SchemeGroupVersion.Identifier(),
Kind: "Deployment",
},
ObjectMeta: metav1.ObjectMeta{
Name: cfg.Name,
Namespace: cfg.Namespace,
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: cfg.Labels,
},
Replicas: ptr.To(cfg.Replicas),
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: cfg.Labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: cfg.Name,
Image: "alpine:latest",
Command: []string{"watch", "echo", "hello world"},
},
},
},
},
},
}
}
type ServiceConfig struct {
Name string
Namespace string
Labels map[string]string
Port int32
TargetPort int
}
func CreateService(cfg ServiceConfig) *corev1.Service {
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Service",
},
ObjectMeta: metav1.ObjectMeta{
Name: cfg.Name,
Namespace: cfg.Namespace,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Selector: cfg.Labels,
Ports: []corev1.ServicePort{
{
Protocol: corev1.ProtocolTCP,
Port: cfg.Port,
TargetPort: intstr.FromInt(cfg.TargetPort),
},
},
},
}
}

Repeating the steps as in the first example, we:

Compile it:

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

Deploy it:

Terminal window
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. We may break things out as a reusable functions, create our own contracts, and write tests. The world is your oyster.

In this example, we used the the k8s api packages to build our resource values. However you may use the types resource types directly, or create your own simplified representations.