Skip to content
This repository has been archived by the owner on Dec 5, 2022. It is now read-only.

WIP: Initial version of golang port #41

Closed
wants to merge 1 commit into from
Closed

Conversation

alexlarsson
Copy link
Collaborator

This is not necessarily meant to go in this repo, but this is an initial cut at porting all of quadlet into golang, for eventual integration with the podman project.

You can build it with go build github.com/containers/quadlet/cmd/quadlet and then run the tests from the "tests" subdirectory with: ./testcase-runner.py cases/ /path/to/golang/quadlet

@rhatdan
Copy link
Member

rhatdan commented Sep 16, 2022

@vrothberg PTAL

cmd/quadlet/main.go Outdated Show resolved Hide resolved
pkg/quadlet/podmancmdline.go Outdated Show resolved Hide resolved
pkg/quadlet/podmancmdline.go Outdated Show resolved Hide resolved
pkg/quadlet/podmancmdline.go Show resolved Hide resolved
pkg/quadlet/quadlet.go Outdated Show resolved Hide resolved
pkg/quadlet/quadlet.go Show resolved Hide resolved
pkg/quadlet/quadlet.go Outdated Show resolved Hide resolved
pkg/quadlet/quadlet.go Outdated Show resolved Hide resolved
@alexlarsson
Copy link
Collaborator Author

Fixed the comments from @ygalblum and rebased

@vrothberg
Copy link
Member

I just ran golangci-lint run (https://golangci-lint.run/) before reading the code and it reports a number of issues which need to be fixed before we can move it over to containers/podman. The linter is a strict enforcement in podman.

Copy link
Member

@vrothberg vrothberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First round of reviews. Need to stop here due to time constraints but will continue reviewing podmancmdline.go ff tomorrow.

files, err := os.ReadDir(sourcePath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
Log("Can't read \"%s\": %s", sourcePath, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend returning errors and let the callers take care of logging or considering it fatal. Only logging errors quickly turns into spaghetti code.

Applies to other parts of the code as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by callers? This is the main.go file of a systemd generator. There is no caller of this code. And it has to log, directly to /dev/kmsg, because it is running in an extremely limited environment during early boot where there is no other way to communicate to anything at all.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referring to code hygiene and being more Go idiomatic.

if err != nil {
    Log(err)
}
return

should be

if err != nil {
    return err
}

and let the callers of this function do the logging. It certainly works as is but it is very uncommon in Go - it loves throwing its errors around.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe that makes sense for this error, but later in this function we log parse error for each .container file, but then continue loading the others. So, for that case we need to log directly.

}
}

func getUnitDirs(user bool) []string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd appreciate a verbose comment on this function (and many others) to be able to match intent vs. implementation.

Other container tools such as Podman et al. will read system configurations (/etc/containers/) even when being run as a ordinary rootless user. man containers.conf, for example, will read /etc/containers/containers.conf unless there is a $HOME/.config/containers/containers.conf.

I do not have an opinion on how Quadlet should handle that but I think should be documented in the code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of this function is to figure out where to read the *.container files from. This is triggered by whether the quadlet process is started as /usr/lib/systemd/system-generators/quadlet-system-generator, or /usr/lib/systemd/system-generators/quadlet-user-generator.

In the earlier case it will always return /etc/containers/systemd and /usr/share/containers/systemd (actually these are build time options in the C version), and in the user case it is $XDG_CONFIG_HOME/containers/systemd. It additionally respects an env var, but that is only used to drive the CI.

We can't do things like read /etc/containers.conf during the generators. It can reference all sort of stuff that are not available yet. See here for some basic rules: https://www.freedesktop.org/software/systemd/man/systemd.generator.html#Notes%20about%20writing%20generators

In addition, the generator phase completely blocks the boot of the entire system, and until all generators are done systemd won't even be trying to parse any unit files. Any preformance problem here significantly drags down the boot time of the entire system, which is very bad. Just going from C to golang is likely to make quadlet the slowest generator (due to binary size).

var isUser bool

var noKmsg bool = false
var kmsgFile *os.File = nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving that into a

var (

)

along with comments


for _, file := range files {
name := file.Name()
if units[name] == nil &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Quadlet should log when units[name] != nil (i.e., a unit-name clash across multiple directories). It may be a simple oversight of the user who may be tricked into believing "it just works". Debugging that scenario can be time intensive as there will be a service running with the expected name but with an unexpected workload.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how all systemd overriding works though. If you have a /usr/ version of the .container file you can override that as sysadmin by adding one in /etc.


defer f.Close()

err = service.Write(f)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: This seems to be point where Quadlet and podman generate systemd can share logic. (didn't read the entire code yet)

return nil
}

func enableServiceFile(outputPath string, service *systemdparser.UnitFile) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: I do not understand what this function does but did not inspect the callees yet.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implements the [Install] section in the .container unit file. This is a section that is otherwise implemented by the "systemctl enable" call. See #20 for the origin of this.

prgname := path.Base(os.Args[0])
isUser = strings.Index(prgname, "user") != -1

flag.Parse()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest moving the var decls, the flag initializing and main() closer. It's a bit scattered at the moment and wont' fit on one screen.

continue
}

if service == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if service == nil {
if err != nil {

go idiomatic


os.MkdirAll(outputPath, os.ModePerm)

for name, unit := range units {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider turning the body into a function that returns an error.

@@ -0,0 +1,3 @@
module github.com/containers/quadlet

go 1.18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this go.mod file. No dependencies are good dependencies.


const (
UnitDirAdmin = "/etc/containers/systemd"
UnitDirDistro = "/usr/share/containers/systemd"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did not read the rest of the code below, but I'd expect a UnitDirUser in $HOME such that a user can configure their own services without root privileges.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is this code from main:

        if user {
                if configDir, err := os.UserConfigDir(); err == nil {
                        dirs = append(dirs, path.Join(configDir, "containers/systemd"))
                }
        } else {

Hard to do as a const though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why that would be better though. That just means everything that links to this computes this at init time whether used or not. (This over-reliance on running random code during init is imho a problem with golang in general.) Better would be to have a function to get it. But maybe we should wait until there is an actual external user and see what is best.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get free out of jail card is to mark Quadlet as "tech preview" until we have figured such things out.


var validPortRange = regexp.MustCompile(`\d+(-\d+)?(/udp|/tcp)?$`)

var supportedContainerKeys = map[string]bool{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the keys can be consts and shared with other locations using the strings.

return f
}

func ParseUnitFile(pathName string) (*UnitFile, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can simplify the parser. Couldn't we require the .container files to start with the [container] table? This way, Quadlet only had to parse the [container] table and leave the rest untouched (i.e., by parsing until EOF or the next table).

It seems to be possible to specify the same table multiple times, so I imagine:

[container]
...
[unit from user]

can be turned into

[unit from user]
[unit from quadlet]
[service from quadlet]

I think this could simplify the code quite a bit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But we modify the service section (and others), as well as parse out details from other sections (like install). I don't see how this would help anything.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, i guess I see how we can e.g. allow the user to say:

[service]
Restart=always

And then when can set the exec line of the service section in a separate copy of it. However, we still do things like read the install section. And anyway, if we can parse one section, we could parse any? Is it really easier to parse one?

Signed-off-by: Alexander Larsson <alexl@redhat.com>
alexlarsson added a commit to alexlarsson/podman that referenced this pull request Oct 3, 2022
Based on the initial port in containers/quadlet#41

Signed-off-by: Alexander Larsson <alexl@redhat.com>
alexlarsson added a commit to alexlarsson/podman that referenced this pull request Oct 4, 2022
Based on the initial port in containers/quadlet#41

Signed-off-by: Alexander Larsson <alexl@redhat.com>
alexlarsson added a commit to alexlarsson/podman that referenced this pull request Oct 5, 2022
Based on the initial port in containers/quadlet#41

Signed-off-by: Alexander Larsson <alexl@redhat.com>
alexlarsson added a commit to alexlarsson/podman that referenced this pull request Oct 14, 2022
Based on the initial port in containers/quadlet#41

Signed-off-by: Alexander Larsson <alexl@redhat.com>
alexlarsson added a commit to alexlarsson/podman that referenced this pull request Oct 17, 2022
Based on the initial port in containers/quadlet#41

This contains the unit tests and the testcases from the C code as well
as modification to the podman spec file based on what the quadlet
spec file looks like, producing a podman-quadlet subpackage.

Signed-off-by: Alexander Larsson <alexl@redhat.com>
@rhatdan
Copy link
Member

rhatdan commented Dec 4, 2022

Since this has been merged into Podman, closing PR.

@rhatdan rhatdan closed this Dec 4, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants