How-to manage workloads on Kubernetes¶
This guide covers how to manage workloads on Kubernetes using goops
. Workloads are services that run in sidecar containers, next to the charm container. The charm uses Pebble to manage these workloads. Pebble is a lightweight Linux service manager that allows the charm to manage services, files, and health checks in the application's container.
1. Declare containers¶
Declare the containers in you charm's charmcraft.yaml
file. For example:
containers:
myapp:
resource: myapp-image
resources:
myapp-image:
type: oci-image
description: OCI image for my application
Note
For more information on the charmcraft.yaml
charm definition, read the official charmcraft documentation.
2. Manage workloads using goops.Pebble
¶
You can manage workloads using the goops.Pebble
API. In the following example, we initialize a Pebble client for the myapp
container, push a configuration file, create a Pebble layer, and start the service.
package charm
import (
"fmt"
"strings"
"github.com/canonical/pebble/client"
"github.com/gruyaume/goops"
"gopkg.in/yaml.v3"
)
const (
ConfigPath = "/etc/config.yaml"
)
type ServiceConfig struct {
Override string `yaml:"override"`
Summary string `yaml:"summary"`
Command string `yaml:"command"`
Startup string `yaml:"startup"`
}
type PebbleLayer struct {
Summary string `yaml:"summary"`
Description string `yaml:"description"`
Services map[string]ServiceConfig `yaml:"services"`
}
type PebblePlan struct {
Services map[string]ServiceConfig `yaml:"services"`
}
func Configure() error {
pebble := goops.Pebble("myapp")
_, err := pebble.SysInfo()
if err != nil {
return fmt.Errorf("could not connect to pebble: %w", err)
}
err = syncConfig(pebble)
if err != nil {
return fmt.Errorf("could not sync config: %w", err)
}
err = syncPebbleService(pebble)
if err != nil {
return fmt.Errorf("could not sync pebble service: %w", err)
}
_ = goops.SetUnitStatus(goops.StatusActive, "service is running")
return nil
}
func syncConfig(pebble goops.PebbleClient) error {
expectedConfig := "Example configuration file for MyApp"
err := pebble.Push(&client.PushOptions{
Source: strings.NewReader(expectedConfig),
Path: ConfigPath,
})
if err != nil {
return fmt.Errorf("could not push file: %w", err)
}
return nil
}
func syncPebbleService(pebble goops.PebbleClient) error {
if !pebbleLayerCreated(pebble) {
goops.LogInfof("Pebble layer not created")
err := addPebbleLayer(pebble)
if err != nil {
return fmt.Errorf("could not add pebble layer: %w", err)
}
goops.LogInfof("Pebble layer created")
}
_, err := pebble.Start(&client.ServiceOptions{
Names: []string{"notary"},
})
if err != nil {
return fmt.Errorf("could not start pebble service: %w", err)
}
goops.LogInfof("Pebble service started")
return nil
}
func pebbleLayerCreated(pebble goops.PebbleClient) bool {
dataBytes, err := pebble.PlanBytes(nil)
if err != nil {
return false
}
var plan PebblePlan
err = yaml.Unmarshal(dataBytes, &plan)
if err != nil {
return false
}
service, exists := plan.Services["myapp"]
if !exists {
return false
}
if service.Command != "myapp --config "+ConfigPath {
return false
}
return true
}
func addPebbleLayer(pebble goops.PebbleClient) error {
layerData, err := yaml.Marshal(PebbleLayer{
Summary: "MyApp layer",
Description: "pebble config layer for MyApp",
Services: map[string]ServiceConfig{
"myapp": {
Override: "replace",
Summary: "My App Service",
Command: "myapp --config " + ConfigPath,
Startup: "enabled",
},
},
})
if err != nil {
return fmt.Errorf("could not marshal layer data to YAML: %w", err)
}
err = pebble.AddLayer(&client.AddLayerOptions{
Combine: true,
Label: "myapp",
LayerData: layerData,
})
if err != nil {
return fmt.Errorf("could not add pebble layer: %w", err)
}
return nil
}
Info
Learn more about workload management in Kubernetes charms: