Skip to content

Commit

Permalink
Initial quadlet version integrated in golang
Browse files Browse the repository at this point in the history
Based on the initial port in containers/quadlet#41

Signed-off-by: Alexander Larsson <alexl@redhat.com>
  • Loading branch information
alexlarsson committed Oct 3, 2022
1 parent 84aff62 commit 71636ff
Show file tree
Hide file tree
Showing 11 changed files with 3,228 additions and 0 deletions.
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,16 @@ podman: bin/podman
.PHONY: podman-remote
podman-remote: $(SRCBINDIR)/podman$(BINSFX)

$(SRCBINDIR)/quadlet: $(SOURCES) go.mod go.sum
$(GOCMD) build \
$(BUILDFLAGS) \
$(GO_LDFLAGS) '$(LDFLAGS_PODMAN)' \
-tags "${BUILDTAGS}" \
-o $@ ./cmd/quadlet

.PHONY: quadlet
quadlet: bin/quadlet

PHONY: podman-remote-static
podman-remote-static: $(SRCBINDIR)/podman-remote-static

Expand Down
254 changes: 254 additions & 0 deletions cmd/quadlet/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package main

import (
"errors"
"flag"
"fmt"
"os"
"path"
"path/filepath"
"strings"

"github.com/containers/podman/v4/pkg/quadlet"
"github.com/containers/podman/v4/pkg/systemdparser"
)

var (
verboseFlag bool // True if -v passed
isUser bool // True if run as quadlet-user-generator executable
)

var (
// data saved between logToKmsg calls
noKmsg = false
kmsgFile *os.File
)

func logToKmsg(s string) bool {
if noKmsg {
return false
}

if kmsgFile == nil {
f, err := os.OpenFile("/dev/kmsg", os.O_WRONLY, 0644)
if err != nil {
noKmsg = true
return false
}
kmsgFile = f
}

if _, err := kmsgFile.Write([]byte(s)); err != nil {
kmsgFile.Close()
kmsgFile = nil
return false
}

return true
}

func Logf(format string, a ...interface{}) {
s := fmt.Sprintf(format, a...)
line := fmt.Sprintf("quadlet-generator[%d]: %s", os.Getpid(), s)

if !logToKmsg(line) {
// If we can't log, print to stderr
fmt.Fprintf(os.Stderr, "%s\n", line)
os.Stderr.Sync()
}
}

var debugEnabled = false

func enableDebug() {
debugEnabled = true
}

func Debugf(format string, a ...interface{}) {
if debugEnabled {
Logf(format, a...)
}
}

func getUnitDirs(user bool) []string {
unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS")
if len(unitDirsEnv) > 0 {
return strings.Split(unitDirsEnv, ":")
}

dirs := make([]string, 0)
if user {
if configDir, err := os.UserConfigDir(); err == nil {
dirs = append(dirs, path.Join(configDir, "containers/systemd"))
}
} else {
dirs = append(dirs, quadlet.UnitDirAdmin)
dirs = append(dirs, quadlet.UnitDirDistro)
}
return dirs
}

func loadUnitsFromDir(sourcePath string, units map[string]*systemdparser.UnitFile) {
files, err := os.ReadDir(sourcePath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
Logf("Can't read \"%s\": %s", sourcePath, err)
}
return
}

for _, file := range files {
name := file.Name()
if units[name] == nil &&
(strings.HasSuffix(name, ".container") ||
strings.HasSuffix(name, ".volume")) {
path := path.Join(sourcePath, name)

Debugf("Loading source unit file %s", path)

if f, err := systemdparser.ParseUnitFile(path); err != nil {
Logf("Error loading '%s', ignoring: %s", path, err)
} else {
units[name] = f
}
}
}
}

func generateServiceFile(service *systemdparser.UnitFile) error {
Debugf("writing '%s'", service.Path)

service.PrependComment("",
"Automatically generated by quadlet-generator",
"")

f, err := os.Create(service.Path)
if err != nil {
return err
}

defer f.Close()

err = service.Write(f)
if err != nil {
return err
}

err = f.Sync()
if err != nil {
return err
}

return nil
}

func enableServiceFile(outputPath string, service *systemdparser.UnitFile) {
symlinks := make([]string, 0)

aliases := service.LookupAllStrv(quadlet.InstallGroup, "Alias")
for _, alias := range aliases {
symlinks = append(symlinks, filepath.Clean(alias))
}

wantedBy := service.LookupAllStrv(quadlet.InstallGroup, "WantedBy")
for _, wantedByUnit := range wantedBy {
// Only allow filenames, not paths
if !strings.Contains(wantedByUnit, "/") {
symlinks = append(symlinks, fmt.Sprintf("%s.wants/%s", wantedByUnit, service.Filename))
}
}

requiredBy := service.LookupAllStrv(quadlet.InstallGroup, "RequiredBy")
for _, requiredByUnit := range requiredBy {
// Only allow filenames, not paths
if !strings.Contains(requiredByUnit, "/") {
symlinks = append(symlinks, fmt.Sprintf("%s.requires/%s", requiredByUnit, service.Filename))
}
}

for _, symlinkRel := range symlinks {
target, err := filepath.Rel(path.Dir(symlinkRel), service.Filename)
if err != nil {
Logf("Can't create symlink %s: %s", symlinkRel, err)
continue
}
symlinkPath := path.Join(outputPath, symlinkRel)

symlinkDir := path.Dir(symlinkPath)
err = os.MkdirAll(symlinkDir, os.ModePerm)
if err != nil {
Logf("Can't create dir %s: %s", symlinkDir, err)
continue
}

Debugf("Creating symlink %s -> %s", symlinkPath, target)
_ = os.Remove(symlinkPath) // overwrite existing symlinks
err = os.Symlink(target, symlinkPath)
if err != nil {
Logf("Failed creating symlink %s: %s", symlinkPath, err)
}
}
}

func main() {
prgname := path.Base(os.Args[0])
isUser = strings.Contains(prgname, "user")

flag.Parse()

if verboseFlag {
enableDebug()
}

if flag.NArg() < 1 {
Logf("Missing output directory argument")
os.Exit(1)
}

outputPath := flag.Arg(0)

Debugf("Starting quadlet-generator, output to: %s", outputPath)

sourcePaths := getUnitDirs(isUser)

units := make(map[string]*systemdparser.UnitFile)
for _, d := range sourcePaths {
loadUnitsFromDir(d, units)
}

err := os.MkdirAll(outputPath, os.ModePerm)
if err != nil {
Logf("Can't create dir %s: %s", outputPath, err)
os.Exit(1)
}

for name, unit := range units {
var service *systemdparser.UnitFile
var err error

switch {
case strings.HasSuffix(name, ".container"):
service, err = quadlet.ConvertContainer(unit, isUser)
case strings.HasSuffix(name, ".volume"):
service, err = quadlet.ConvertVolume(unit, name)
default:
Logf("Unsupported file type '%s'", name)
continue
}

if err != nil {
Logf("Error converting '%s', ignoring: %s", name, err)
} else {
service.Path = path.Join(outputPath, service.Filename)

if err := generateServiceFile(service); err != nil {
Logf("Error writing '%s'o: %s", service.Path, err)
}
enableServiceFile(outputPath, service)
}
}
}

func init() {
flag.BoolVar(&verboseFlag, "v", false, "Print debug information")
}
60 changes: 60 additions & 0 deletions pkg/quadlet/podmancmdline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package quadlet

import (
"fmt"
"sort"
)

/* This is a helper for constructing podman commandlines */
type PodmanCmdline struct {
Args []string
}

func (c *PodmanCmdline) add(args ...string) {
c.Args = append(c.Args, args...)
}

func (c *PodmanCmdline) addf(format string, a ...interface{}) {
c.add(fmt.Sprintf(format, a...))
}

func (c *PodmanCmdline) addKeys(arg string, keys map[string]string) {
ks := make([]string, 0, len(keys))
for k := range keys {
ks = append(ks, k)
}
sort.Strings(ks)

for _, k := range ks {
c.add(arg, fmt.Sprintf("%s=%s", k, keys[k]))
}
}

func (c *PodmanCmdline) addEnv(env map[string]string) {
c.addKeys("--env", env)
}

func (c *PodmanCmdline) addLabels(labels map[string]string) {
c.addKeys("--label", labels)
}

func (c *PodmanCmdline) addAnnotations(annotations map[string]string) {
c.addKeys("--annotation", annotations)
}

func (c *PodmanCmdline) addIDMap(argPrefix string, containerIDStart, hostIDStart, numIDs uint32) {
if numIDs != 0 {
c.add(argPrefix)
c.addf("%d:%d:%d", containerIDStart, hostIDStart, numIDs)
}
}

func NewPodmanCmdline(args ...string) *PodmanCmdline {
c := &PodmanCmdline{
Args: make([]string, 0),
}

c.add("/usr/bin/podman")
c.add(args...)
return c
}
Loading

0 comments on commit 71636ff

Please sign in to comment.