-
Notifications
You must be signed in to change notification settings - Fork 84
Quadlet sugar #694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Quadlet sugar #694
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -100,9 +100,13 @@ func (c Config) ToIgn3_7Unvalidated(options common.TranslateOptions) (types.Conf | |
|
|
||
| c.addMountUnits(&ret, &tm) | ||
|
|
||
| tm2, r2 := c.processTrees(&ret, options) | ||
| tm.Merge(tm2) | ||
| r.Merge(r2) | ||
| tmTrees, rTrees := c.processTrees(&ret, options) | ||
| tmQuadlets, rQuadlets := c.processQuadlets(&ret, options) | ||
|
|
||
| tm.Merge(tmTrees) | ||
| tm.Merge(tmQuadlets) | ||
| r.Merge(rTrees) | ||
| r.Merge(rQuadlets) | ||
|
|
||
| if r.IsFatal() { | ||
| return types.Config{}, translate.TranslationSet{}, r | ||
|
|
@@ -297,6 +301,172 @@ func translateDropin(from Dropin, options common.TranslateOptions) (to types.Dro | |
| return | ||
| } | ||
|
|
||
| // buildQuadletPath returns the filesystem path for a quadlet. | ||
| // See https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html | ||
| func buildQuadletPath(isRoot bool, quadletName string) string { | ||
| const ( | ||
| adminContainersPath = "/etc/containers/systemd" | ||
| userContainersPath = "/etc/containers/systemd/users" | ||
| ) | ||
| var base string | ||
| if isRoot { | ||
| base = adminContainersPath | ||
| } else { | ||
| base = userContainersPath | ||
| } | ||
| return slashpath.Join(base, quadletName) | ||
| } | ||
|
|
||
| // isTemplateInstance checks if a quadlet name is a template instance (e.g. foo@100.container). | ||
| // Returns true and the base template name (e.g. foo@.container) if it is an instance. | ||
| func isTemplateInstance(name string) (bool, string) { | ||
| splitIndex := strings.Index(name, "@") | ||
| if splitIndex == -1 { | ||
| return false, "" | ||
| } | ||
| extensionIndex := strings.LastIndex(name, ".") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The validation shuld catch this, as it checks for extension, to make the function less fragile, I have added a check for |
||
| if extensionIndex == -1 || splitIndex+1 == extensionIndex { | ||
| return false, "" | ||
| } | ||
| baseName := name[:splitIndex] | ||
| extension := name[extensionIndex+1:] | ||
| templateName := fmt.Sprintf("%s@.%s", baseName, extension) | ||
| return true, templateName | ||
| } | ||
|
|
||
| // readLocalOrInlineContents reads content from either a local file or inline string (see Quadlet and Dropin). | ||
| // Returns the content as bytes, the source path for error reporting, and any errors. | ||
| func readLocalOrInlineContents(contentsLocal, contentsInline *string, ctxPath path.ContextPath, options common.TranslateOptions) (content []byte, contentPath path.ContextPath, err error) { | ||
| if util.NotEmpty(contentsLocal) { | ||
| contentPath = ctxPath.Append("contents_local") | ||
| localContents, err := baseutil.ReadLocalFile(*contentsLocal, options.FilesDir) | ||
| if err != nil { | ||
| return content, contentPath, err | ||
| } | ||
| content = localContents | ||
| } | ||
|
|
||
| if util.NotEmpty(contentsInline) { | ||
| contentPath = ctxPath.Append("contents") | ||
| content = []byte(*contentsInline) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| // addFileWithContents reads content (local or inline) and creates a file node in the tracker. | ||
| // Used for both quadlet files and their drop-ins, both of which will have either contentsLocal, or contents, but not both. | ||
| func addFileWithContents( | ||
| contentsLocal, inlineContents *string, | ||
| destPath string, | ||
| ctxPath path.ContextPath, | ||
| t *nodeTracker, | ||
| options common.TranslateOptions, | ||
| ) (translate.TranslationSet, report.Report) { | ||
| var r report.Report | ||
| ts := translate.NewTranslationSet("yaml", "json") | ||
|
|
||
| _, file := t.GetFile(destPath) | ||
| // If the node already exists, we dont want to over-write, we will just error | ||
| if (file != nil && util.NotEmpty(file.Contents.Source)) || t.Exists(destPath) { | ||
| r.AddOnError(ctxPath, common.ErrNodeExists) | ||
| return ts, r | ||
| } | ||
|
|
||
| i, file := t.AddFile(types.File{Node: createNode(destPath, NodeUser{}, NodeGroup{})}) | ||
| if i == 0 { | ||
| ts.AddTranslation(ctxPath, path.New("json", "storage", "files")) | ||
| } | ||
| ts.AddFromCommonSource(ctxPath, path.New("json", "storage", "files", i), file) | ||
| ts.AddTranslation(ctxPath.Append("name"), path.New("json", "storage", "files", i, "path")) | ||
| contentBytes, contentPath, err := readLocalOrInlineContents(contentsLocal, inlineContents, ctxPath, options) | ||
| if err != nil { | ||
| r.AddOnError(contentPath, err) | ||
| return ts, r | ||
| } | ||
| url, compression, err := baseutil.MakeDataURL(contentBytes, file.Contents.Compression, !options.NoResourceAutoCompression) | ||
| if err != nil { | ||
| r.AddOnError(ctxPath, err) | ||
| return ts, r | ||
| } | ||
| file.Contents.Source = &url | ||
| ts.AddTranslation(contentPath, path.New("json", "storage", "files", i, "contents", "source")) | ||
| if compression != nil { | ||
| file.Contents.Compression = compression | ||
| ts.AddTranslation(ctxPath, path.New("json", "storage", "files", i, "contents", "compression")) | ||
| } | ||
| ts.AddTranslation(contentPath, path.New("json", "storage", "files", i, "contents")) | ||
| if file.Mode == nil { | ||
| mode := 0644 | ||
| file.Mode = &mode | ||
| ts.AddTranslation(ctxPath, path.New("json", "storage", "files", i, "mode")) | ||
| } | ||
|
|
||
| return ts, r | ||
| } | ||
|
|
||
| // quadletToSymlink creates a symlink node for a template instance pointing to its base template. | ||
| func quadletToSymlink(quadlet Quadlet, quadletPath path.ContextPath, t *nodeTracker, templateName string) (translate.TranslationSet, report.Report) { | ||
| var r report.Report | ||
| ts := translate.NewTranslationSet("yaml", "json") | ||
|
|
||
| destPath := buildQuadletPath(quadlet.Rootful, quadlet.Name) | ||
| _, link := t.GetLink(destPath) | ||
| // If the node already exists, we don't want to over-write, we will just error | ||
| if link != nil || t.Exists(destPath) { | ||
| r.AddOnError(quadletPath, common.ErrNodeExists) | ||
| return ts, r | ||
| } | ||
|
|
||
| i, link := t.AddLink(types.Link{Node: types.Node{Path: destPath}, LinkEmbedded1: types.LinkEmbedded1{ | ||
| Target: &templateName, | ||
| }}) | ||
| if i == 0 { | ||
| ts.AddTranslation(quadletPath, path.New("json", "storage", "links")) | ||
| } | ||
| ts.AddFromCommonSource(quadletPath, path.New("json", "storage", "links", i), link) | ||
| ts.AddTranslation(quadletPath.Append("name"), path.New("json", "storage", "links", i, "path")) | ||
| ts.AddTranslation(quadletPath, path.New("json", "storage", "links", i, "target")) | ||
| return ts, r | ||
| } | ||
|
|
||
| func (c Config) processQuadlets(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { | ||
| ts := translate.NewTranslationSet("yaml", "json") | ||
| var r report.Report | ||
| if len(c.Systemd.Quadlets) == 0 { | ||
| return ts, r | ||
| } | ||
|
|
||
| t := newNodeTracker(ret) | ||
| quadletsPath := path.New("yaml", "systemd", "quadlets") | ||
| ts.AddTranslation(quadletsPath, path.New("json", "storage")) // quadlets will be translated to storage (files and links) | ||
| for quadletNum, quadlet := range c.Systemd.Quadlets { | ||
| quadletPath := quadletsPath.Append(quadletNum) | ||
|
|
||
| // We need to handle `foo@bar.container` differently than `foo@.container`, as the former needs to be a symlink to the latter | ||
| var tsFile translate.TranslationSet | ||
| var rFile report.Report | ||
| if isTemplate, templateName := isTemplateInstance(quadlet.Name); isTemplate { | ||
| tsFile, rFile = quadletToSymlink(quadlet, quadletPath, t, templateName) | ||
| } else { | ||
| destPath := buildQuadletPath(quadlet.Rootful, quadlet.Name) | ||
| tsFile, rFile = addFileWithContents(quadlet.ContentsLocal, quadlet.Contents, destPath, quadletPath, t, options) | ||
| } | ||
|
|
||
| ts.Merge(tsFile) | ||
| r.Merge(rFile) | ||
|
|
||
| for i, dropin := range quadlet.Dropins { | ||
| dropinPath := quadletPath.Append("dropins").Append(i) | ||
| destPath := buildQuadletPath(quadlet.Rootful, quadlet.Name) + ".d/" + dropin.Name | ||
| tsFile, rFile := addFileWithContents(dropin.ContentsLocal, dropin.Contents, destPath, dropinPath, t, options) | ||
| ts.Merge(tsFile) | ||
| r.Merge(rFile) | ||
| } | ||
| } | ||
|
|
||
| return ts, r | ||
| } | ||
|
|
||
| func (c Config) processTrees(ret *types.Config, options common.TranslateOptions) (translate.TranslationSet, report.Report) { | ||
| ts := translate.NewTranslationSet("yaml", "json") | ||
| var r report.Report | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just want to make sure this is intentional -- per the podman docs,
/etc/containers/systemd/users/makes the quadlet run for all non-root users. If the intent is to target a specific user it would need to be/etc/containers/systemd/users/${UID}.For ignition this is probably the right default, but might be worth calling out in the docs that
rootful: falsemeans "all users" and not a specific user.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this was by design. I actually was not sure wether we shuld just have
/etc/containers/systemd/and remove the users one. This would align more closely with ignition which does not save to/etc/systemd/user/. I do think that it may be good to keep it, as it is not a lot more code when compared to just having the one path.Either way, I will have to document this.