From 3084ed4686f7dc1aa323559064f36c78016f1349 Mon Sep 17 00:00:00 2001 From: Ygal Blum Date: Tue, 22 Nov 2022 11:53:53 +0200 Subject: [PATCH] Quadlet: Add support for .kube files Get the path to the yaml file and call podman kube play Add tests Signed-off-by: Ygal Blum --- cmd/quadlet/main.go | 21 ++++++++-- pkg/systemd/quadlet/quadlet.go | 70 ++++++++++++++++++++++++++++++++++ test/e2e/quadlet/basic.kube | 17 +++++++++ test/e2e/quadlet_test.go | 34 ++++++++++++++--- 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 test/e2e/quadlet/basic.kube diff --git a/cmd/quadlet/main.go b/cmd/quadlet/main.go index 1666b8a4a3..98f80467e5 100644 --- a/cmd/quadlet/main.go +++ b/cmd/quadlet/main.go @@ -34,6 +34,15 @@ var ( kmsgFile *os.File ) +var ( + void struct{} + supportedExtensions = map[string]struct{}{ + ".container": void, + ".volume": void, + ".kube": void, + } +) + // We log directly to /dev/kmsg, because that is the only way to get information out // of the generator into the system logs. func logToKmsg(s string) bool { @@ -105,6 +114,12 @@ func getUnitDirs(user bool) []string { return dirs } +func isExtSupported(filename string) bool { + ext := filepath.Ext(filename) + _, ok := supportedExtensions[ext] + return ok +} + func loadUnitsFromDir(sourcePath string, units map[string]*parser.UnitFile) { files, err := os.ReadDir(sourcePath) if err != nil { @@ -116,9 +131,7 @@ func loadUnitsFromDir(sourcePath string, units map[string]*parser.UnitFile) { for _, file := range files { name := file.Name() - if units[name] == nil && - (strings.HasSuffix(name, ".container") || - strings.HasSuffix(name, ".volume")) { + if units[name] == nil && isExtSupported(name) { path := path.Join(sourcePath, name) Debugf("Loading source unit file %s", path) @@ -322,6 +335,8 @@ func main() { service, err = quadlet.ConvertContainer(unit, isUser) case strings.HasSuffix(name, ".volume"): service, err = quadlet.ConvertVolume(unit, name) + case strings.HasSuffix(name, ".kube"): + service, err = quadlet.ConvertKube(unit) default: Logf("Unsupported file type '%s'", name) continue diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index 6e07fd578a..32f11db73c 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -22,6 +22,8 @@ const ( XContainerGroup = "X-Container" VolumeGroup = "Volume" XVolumeGroup = "X-Volume" + KubeGroup = "Kube" + XKubeGroup = "X-Kube" ) var validPortRange = regexp.MustCompile(`\d+(-\d+)?(/udp|/tcp)?$`) @@ -55,6 +57,7 @@ const ( KeySeccompProfile = "SeccompProfile" KeyAddDevice = "AddDevice" KeyNetwork = "Network" + KeyYaml = "Yaml" ) // Supported keys in "Container" group @@ -95,6 +98,11 @@ var supportedVolumeKeys = map[string]bool{ KeyLabel: true, } +// Supported keys in "Kube" group +var supportedKubeKeys = map[string]bool{ + KeyYaml: true, +} + func replaceExtension(name string, extension string, extraPrefix string, extraSuffix string) string { baseName := name @@ -593,3 +601,65 @@ func ConvertVolume(volume *parser.UnitFile, name string) (*parser.UnitFile, erro return service, nil } + +func ConvertKube(kube *parser.UnitFile) (*parser.UnitFile, error) { + service := kube.Dup() + service.Filename = replaceExtension(kube.Filename, ".service", "", "") + + if kube.Path != "" { + service.Add(UnitGroup, "SourcePath", kube.Path) + } + + if err := checkForUnknownKeys(kube, KubeGroup, supportedKubeKeys); err != nil { + return nil, err + } + + // Rename old Kube group to x-Kube so that systemd ignores it + service.RenameGroup(KubeGroup, XKubeGroup) + + yamlPath, ok := kube.Lookup(KubeGroup, KeyYaml) + if !ok || len(yamlPath) == 0 { + return nil, fmt.Errorf("no Yaml key specified") + } + + // Only allow mixed or control-group, as nothing else works well + killMode, ok := service.Lookup(ServiceGroup, "KillMode") + if !ok || !(killMode == "mixed" || killMode == "control-group") { + if ok { + return nil, fmt.Errorf("invalid KillMode '%s'", killMode) + } + + // We default to mixed instead of control-group, because it lets conmon do its thing + service.Set(ServiceGroup, "KillMode", "mixed") + } + + // Set PODMAN_SYSTEMD_UNIT so that podman auto-update can restart the service. + service.Add(ServiceGroup, "Environment", "PODMAN_SYSTEMD_UNIT=%n") + + // Need the containers filesystem mounted to start podman + service.Add(UnitGroup, "RequiresMountsFor", "%t/containers") + + service.Setv(ServiceGroup, + "Type", "notify", + "NotifyAccess", "all") + + execStart := NewPodmanCmdline("kube", "play") + + execStart.add( + // Replace any previous container with the same name, not fail + "--replace", + + // Use a service container + "--service-container=true", + ) + + execStart.add(yamlPath) + + service.AddCmdline(ServiceGroup, "ExecStart", execStart.Args) + + execStop := NewPodmanCmdline("kube", "down") + execStop.add(yamlPath) + service.AddCmdline(ServiceGroup, "ExecStop", execStop.Args) + + return service, nil +} diff --git a/test/e2e/quadlet/basic.kube b/test/e2e/quadlet/basic.kube new file mode 100644 index 0000000000..f514ea6088 --- /dev/null +++ b/test/e2e/quadlet/basic.kube @@ -0,0 +1,17 @@ +## assert-podman-args "kube" +## assert-podman-args "play" +## assert-podman-final-args deployment.yml +## assert-podman-args "--replace" +## assert-podman-args "--service-container=true" +## assert-podman-stop-args "kube" +## assert-podman-stop-args "down" +## assert-podman-stop-final-args deployment.yml +## assert-key-is "Unit" "RequiresMountsFor" "%t/containers" +## assert-key-is "Service" "KillMode" "mixed" +## assert-key-is "Service" "Type" "notify" +## assert-key-is "Service" "NotifyAccess" "all" +## assert-key-is "Service" "Environment" "PODMAN_SYSTEMD_UNIT=%n" + + +[Kube] +Yaml=deployment.yml diff --git a/test/e2e/quadlet_test.go b/test/e2e/quadlet_test.go index e7f377db67..ddec06fd77 100644 --- a/test/e2e/quadlet_test.go +++ b/test/e2e/quadlet_test.go @@ -110,19 +110,35 @@ func (t *quadletTestcase) assertKeyContains(args []string, unit *parser.UnitFile return ok && strings.Contains(realValue, value) } -func (t *quadletTestcase) assertPodmanArgs(args []string, unit *parser.UnitFile) bool { - podmanArgs, _ := unit.LookupLastArgs("Service", "ExecStart") +func (t *quadletTestcase) assertPodmanArgs(args []string, unit *parser.UnitFile, key string) bool { + podmanArgs, _ := unit.LookupLastArgs("Service", key) return findSublist(podmanArgs, args) != -1 } -func (t *quadletTestcase) assertFinalArgs(args []string, unit *parser.UnitFile) bool { - podmanArgs, _ := unit.LookupLastArgs("Service", "ExecStart") +func (t *quadletTestcase) assertPodmanFinalArgs(args []string, unit *parser.UnitFile, key string) bool { + podmanArgs, _ := unit.LookupLastArgs("Service", key) if len(podmanArgs) < len(args) { return false } return matchSublistAt(podmanArgs, len(podmanArgs)-len(args), args) } +func (t *quadletTestcase) assertStartPodmanArgs(args []string, unit *parser.UnitFile) bool { + return t.assertPodmanArgs(args, unit, "ExecStart") +} + +func (t *quadletTestcase) assertStartPodmanFinalArgs(args []string, unit *parser.UnitFile) bool { + return t.assertPodmanFinalArgs(args, unit, "ExecStart") +} + +func (t *quadletTestcase) assertStopPodmanArgs(args []string, unit *parser.UnitFile) bool { + return t.assertPodmanArgs(args, unit, "ExecStop") +} + +func (t *quadletTestcase) assertStopPodmanFinalArgs(args []string, unit *parser.UnitFile) bool { + return t.assertPodmanFinalArgs(args, unit, "ExecStop") +} + func (t *quadletTestcase) assertSymlink(args []string, unit *parser.UnitFile) bool { symlink := args[0] expectedTarget := args[1] @@ -161,11 +177,15 @@ func (t *quadletTestcase) doAssert(check []string, unit *parser.UnitFile, sessio case "assert-key-contains": ok = t.assertKeyContains(args, unit) case "assert-podman-args": - ok = t.assertPodmanArgs(args, unit) + ok = t.assertStartPodmanArgs(args, unit) case "assert-podman-final-args": - ok = t.assertFinalArgs(args, unit) + ok = t.assertStartPodmanFinalArgs(args, unit) case "assert-symlink": ok = t.assertSymlink(args, unit) + case "assert-podman-stop-args": + ok = t.assertStopPodmanArgs(args, unit) + case "assert-podman-stop-final-args": + ok = t.assertStopPodmanFinalArgs(args, unit) default: return fmt.Errorf("Unsupported assertion %s", op) } @@ -300,6 +320,8 @@ var _ = Describe("quadlet system generator", func() { Entry("basic.volume", "basic.volume"), Entry("label.volume", "label.volume"), Entry("uid.volume", "uid.volume"), + + Entry("Basic kube", "basic.kube"), ) })