Skip to content

Commit

Permalink
feat: add uninstaller
Browse files Browse the repository at this point in the history
  • Loading branch information
ctrox committed Oct 13, 2023
1 parent 156d8c2 commit 704a004
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 47 deletions.
170 changes: 123 additions & 47 deletions cmd/installer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"errors"
"flag"
"fmt"
"log"
Expand All @@ -24,10 +25,12 @@ import (
)

var (
criuImage = flag.String("criu-image", "ghcr.io/ctrox/zeropod-criu:a2c4dd2", "criu image to use.")
criuNFTables = flag.Bool("criu-nftables", true, "use criu with nftables")
runtime = flag.String("runtime", "containerd", "specifies which runtime to configure. containerd/k3s/rke2")
hostOptPath = flag.String("host-opt-path", "/opt/zeropod", "path where zeropod binaries are stored on the host")
criuImage = flag.String("criu-image", "ghcr.io/ctrox/zeropod-criu:a2c4dd2", "criu image to use.")
criuNFTables = flag.Bool("criu-nftables", true, "use criu with nftables")
runtime = flag.String("runtime", "containerd", "specifies which runtime to configure. containerd/k3s/rke2")
hostOptPath = flag.String("host-opt-path", "/opt/zeropod", "path where zeropod binaries are stored on the host")
uninstall = flag.Bool("uninstall", false, "uninstalls zeropod by cleaning up all the files the installer created")
installTimeout = flag.Duration("timeout", time.Minute, "duration the installer waits for the installation to complete")
)

type containerRuntime string
Expand All @@ -37,18 +40,19 @@ const (
runtimeRKE2 containerRuntime = "rke2"
runtimeK3S containerRuntime = "k3s"

optPath = "/opt/zeropod"
binPath = "bin/"
criuConfigFile = "/etc/criu/default.conf"
shimBinaryName = "containerd-shim-zeropod-v2"
runtimePath = "/build/" + shimBinaryName
containerdConfig = "/etc/containerd/config.toml"
templateSuffix = ".tmpl"
runtimeClassName = "zeropod"
runtimeHandler = "zeropod"
defaultCriuBin = "criu"
criuIPTablesBin = "criu-iptables"
criuConfig = `tcp-close
optPath = "/opt/zeropod"
binPath = "bin/"
criuConfigFile = "/etc/criu/default.conf"
shimBinaryName = "containerd-shim-zeropod-v2"
runtimePath = "/build/" + shimBinaryName
containerdConfig = "/etc/containerd/config.toml"
configBackupSuffix = ".original"
templateSuffix = ".tmpl"
runtimeClassName = "zeropod"
runtimeHandler = "zeropod"
defaultCriuBin = "criu"
criuIPTablesBin = "criu-iptables"
criuConfig = `tcp-close
skip-in-flight
network-lock skip
`
Expand All @@ -75,19 +79,37 @@ network-lock skip
func main() {
flag.Parse()

if err := installCriu(); err != nil {
client, err := inClusterClient()
if err != nil {
log.Fatalf("unable to create in-cluster client: %s", err)
}

ctx, cancel := context.WithTimeout(context.Background(), *installTimeout)
defer cancel()

if *uninstall {
if err := runUninstall(ctx, client); err != nil {
log.Fatalf("error uninstalling zeropod: %s", err)
}

log.Println("uninstaller completed")
// we exit after uninstall as this should be run as a one-time job
os.Exit(0)
}

if err := installCriu(ctx); err != nil {
log.Fatalf("error installing criu: %s", err)
}

log.Println("installed criu binaries")

if err := installRuntime(containerRuntime(*runtime)); err != nil {
if err := installRuntime(ctx, containerRuntime(*runtime)); err != nil {
log.Fatalf("error installing runtime: %s", err)
}

log.Println("installed runtime")

if err := installRuntimeClass(); err != nil {
if err := installRuntimeClass(ctx, client); err != nil {
log.Fatalf("error installing zeropod runtimeClass: %s", err)
}

Expand All @@ -106,14 +128,12 @@ func main() {
<-quitChannel
}

func installCriu() error {
func installCriu(ctx context.Context) error {
client, err := containerd.New("/run/containerd/containerd.sock", containerd.WithDefaultNamespace("k8s"))
if err != nil {
return err
}

ctx := context.Background()

image, err := client.Pull(ctx, *criuImage)
if err != nil {
return err
Expand Down Expand Up @@ -149,12 +169,10 @@ func installCriu() error {
return nil
}

func installRuntime(runtime containerRuntime) error {
func installRuntime(ctx context.Context, runtime containerRuntime) error {
log.Printf("installing runtime for %s", runtime)

ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
conn, err := dbus.NewSystemdConnectionContext(context.Background())
conn, err := dbus.NewSystemdConnectionContext(ctx)
if err != nil {
return fmt.Errorf("unable to connect to dbus: %w", err)
}
Expand All @@ -176,7 +194,7 @@ func installRuntime(runtime containerRuntime) error {
return fmt.Errorf("unable to write shim file: %w", err)
}

restartRequired, err := configureContainerd(runtime)
restartRequired, err := configureContainerd(runtime, containerdConfig)
if err != nil {
return fmt.Errorf("unable to configure containerd: %w", err)
}
Expand Down Expand Up @@ -224,7 +242,7 @@ func restartUnit(ctx context.Context, conn *dbus.Conn, service string) error {
return nil
}

func configureContainerd(runtime containerRuntime) (restartRequired bool, err error) {
func configureContainerd(runtime containerRuntime, containerdConfig string) (restartRequired bool, err error) {
conf := &config.Config{}
if err := config.LoadConfig(containerdConfig, conf); err != nil {
return false, err
Expand All @@ -237,19 +255,22 @@ func configureContainerd(runtime containerRuntime) (restartRequired bool, err er
}
}

containerdCfg := containerdConfig
// backup the original config
if err := copyConfig(containerdConfig, containerdConfig+configBackupSuffix); err != nil {
return false, err
}

if runtime == runtimeRKE2 || runtime == runtimeK3S {
// for rke2/k3s the containerd config has to be customized via the
// config.toml.tmpl file. So we make a copy of the original config and
// insert our shim config into the template.
if out, err := exec.Command("cp", containerdCfg, containerdCfg+templateSuffix).CombinedOutput(); err != nil {
if out, err := exec.Command("cp", containerdConfig, containerdConfig+templateSuffix).CombinedOutput(); err != nil {
return false, fmt.Errorf("unable to copy config.toml to template: %s: %w", out, err)
}
containerdCfg = containerdConfig + templateSuffix
containerdConfig = containerdConfig + templateSuffix
}

cfg, err := os.OpenFile(containerdCfg, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
cfg, err := os.OpenFile(containerdConfig, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return false, err
}
Expand All @@ -258,7 +279,7 @@ func configureContainerd(runtime containerRuntime) (restartRequired bool, err er
return false, err
}

configured, err := optConfigured()
configured, err := optConfigured(containerdConfig)
if err != nil {
return false, err
}
Expand All @@ -272,7 +293,38 @@ func configureContainerd(runtime containerRuntime) (restartRequired bool, err er
return true, nil
}

func optConfigured() (bool, error) {
func restoreContainerdConfig() error {
if _, err := os.Stat(containerdConfig + configBackupSuffix); err != nil {
if errors.Is(err, os.ErrNotExist) {
log.Println("could not find config backup, either it has already been restored or it never existed")
return nil
}
}

if err := copyConfig(containerdConfig+configBackupSuffix, containerdConfig); err != nil {
return err
}

if err := os.Remove(containerdConfig + configBackupSuffix); err != nil {
return err
}

return nil
}

func copyConfig(from, to string) error {
originalConfig, err := os.ReadFile(from)
if err != nil {
return fmt.Errorf("could not read containerd config: %w", err)
}
if err := os.WriteFile(to, originalConfig, os.ModePerm); err != nil {
return fmt.Errorf("could not write config backup: %w", err)
}

return nil
}

func optConfigured(containerdConfig string) (bool, error) {
conf := &config.Config{}
if err := config.LoadConfig(containerdConfig, conf); err != nil {
return false, err
Expand All @@ -286,28 +338,52 @@ func optConfigured() (bool, error) {
return false, nil
}

func installRuntimeClass() error {
func installRuntimeClass(ctx context.Context, client kubernetes.Interface) error {
runtimeClass := &nodev1.RuntimeClass{
ObjectMeta: v1.ObjectMeta{Name: runtimeClassName},
Handler: runtimeHandler,
}

if _, err := client.NodeV1().RuntimeClasses().Create(ctx, runtimeClass, v1.CreateOptions{}); err != nil {
if !kerrors.IsAlreadyExists(err) {
return err
}
}

return nil
}

func removeRuntimeClass(ctx context.Context, client kubernetes.Interface) error {
if err := client.NodeV1().RuntimeClasses().Delete(ctx, runtimeClassName, v1.DeleteOptions{}); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
}
return nil
}

func inClusterClient() (kubernetes.Interface, error) {
config, err := rest.InClusterConfig()
if err != nil {
return err
return nil, err
}

clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return kubernetes.NewForConfig(config)
}

// runUninstall removes all components installed by zeropod and restores the
// original configuration.
func runUninstall(ctx context.Context, client kubernetes.Interface) error {
if err := removeRuntimeClass(ctx, client); err != nil {
return err
}

runtimeClass := &nodev1.RuntimeClass{
ObjectMeta: v1.ObjectMeta{Name: runtimeClassName},
Handler: runtimeHandler,
if err := os.RemoveAll(optPath); err != nil {
return fmt.Errorf("removing opt path: %w", err)
}

if _, err := clientset.NodeV1().RuntimeClasses().Create(
context.Background(), runtimeClass, v1.CreateOptions{},
); err != nil {
if !kerrors.IsAlreadyExists(err) {
return err
}
if err := restoreContainerdConfig(); err != nil {
return err
}

return nil
Expand Down
75 changes: 75 additions & 0 deletions cmd/installer/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// kindContainerdConfig was extracted from a Kind cluster
const kindContainerdConfig = `
# explicitly use v2 config format
version = 2
[proxy_plugins]
# fuse-overlayfs is used for rootless
[proxy_plugins."fuse-overlayfs"]
type = "snapshot"
address = "/run/containerd-fuse-overlayfs.sock"
[plugins."io.containerd.grpc.v1.cri".containerd]
# save disk space when using a single snapshotter
discard_unpacked_layers = true
# explicitly use default snapshotter so we can sed it in entrypoint
snapshotter = "overlayfs"
# explicit default here, as we're configuring it below
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
# set default runtime handler to v2, which has a per-pod shim
runtime_type = "io.containerd.runc.v2"
# Generated by "ctr oci spec" and modified at base container to mount poduct_uuid
base_runtime_spec = "/etc/containerd/cri-base.json"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
# use systemd cgroup by default
SystemdCgroup = true
# Setup a runtime with the magic name ("test-handler") used for Kubernetes
# runtime class tests ...
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.test-handler]
# same settings as runc
runtime_type = "io.containerd.runc.v2"
base_runtime_spec = "/etc/containerd/cri-base.json"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.test-handler.options]
SystemdCgroup = true
[plugins."io.containerd.grpc.v1.cri"]
# use fixed sandbox image
sandbox_image = "registry.k8s.io/pause:3.7"
# allow hugepages controller to be missing
# see https://github.com/containerd/cri/pull/1501
tolerate_missing_hugepages_controller = true
# restrict_oom_score_adj needs to be true when running inside UserNS (rootless)
restrict_oom_score_adj = true
`

func TestConfigureContainerd(t *testing.T) {
configFile, err := os.CreateTemp("", "containerd-config-*.toml")
require.NoError(t, err)

require.NoError(t, os.WriteFile(configFile.Name(), []byte(kindContainerdConfig), os.ModePerm))

restart, err := configureContainerd(runtimeContainerd, configFile.Name())
require.NoError(t, err)

newFile, err := os.ReadFile(configFile.Name())
require.NoError(t, err)

backupInfo, err := os.Stat(configFile.Name() + configBackupSuffix)
require.NoError(t, err)

assert.NotEmpty(t, backupInfo.Size())
assert.NotEmpty(t, newFile)
assert.True(t, restart)
}
1 change: 1 addition & 0 deletions config/base/rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ rules:
- runtimeclasses
verbs:
- create
- delete
- update
---
apiVersion: rbac.authorization.k8s.io/v1
Expand Down
6 changes: 6 additions & 0 deletions config/uninstall/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resources:
- uninstaller.yaml
images:
- name: installer
newName: ghcr.io/ctrox/zeropod-installer
newTag: dev
Loading

0 comments on commit 704a004

Please sign in to comment.