diff --git a/base/v0_8_exp/schema.go b/base/v0_8_exp/schema.go index c5c1046a..219c8aa9 100644 --- a/base/v0_8_exp/schema.go +++ b/base/v0_8_exp/schema.go @@ -230,7 +230,8 @@ type Storage struct { } type Systemd struct { - Units []Unit `yaml:"units"` + Units []Unit `yaml:"units"` + Quadlets []Quadlet `yaml:"quadlets" butane:"auto_skip"` // Added, not in ignition spec } type Tang struct { @@ -266,6 +267,14 @@ type Unit struct { Name string `yaml:"name"` } +type Quadlet struct { + Contents *string `yaml:"contents"` // file contents + ContentsLocal *string `yaml:"contents_local"` // path to the file + Name string `yaml:"name"` + Rootful bool `yaml:"rootful,omitempty"` + Dropins []Dropin `yaml:"dropins,omitempty"` +} + type Verification struct { Hash *string `yaml:"hash"` } diff --git a/base/v0_8_exp/translate.go b/base/v0_8_exp/translate.go index 0c3ebbda..1c78842d 100644 --- a/base/v0_8_exp/translate.go +++ b/base/v0_8_exp/translate.go @@ -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, ".") + if 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 isTemplateInstance, templateName := isTemplateInstance(quadlet.Name); isTemplateInstance { + 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 diff --git a/base/v0_8_exp/translate_test.go b/base/v0_8_exp/translate_test.go index f9f8cc92..787a34cb 100644 --- a/base/v0_8_exp/translate_test.go +++ b/base/v0_8_exp/translate_test.go @@ -2517,3 +2517,326 @@ func TestToIgn3_7(t *testing.T) { }) } } + +func TestTranslateQuadlets(t *testing.T) { + const ( + SleepContainer = `[Unit] +Description=A sleepy container +[Container] +ContainerName=sleepy-pod-inf +Image=quay.io/fedora/fedora +Exec=sleep infinity +[Install] +WantedBy=multi-user.target` + + SleepContainerAsData = "data:,%5BUnit%5D%0ADescription%3DA%20sleepy%20container%0A%5BContainer%5D%0AContainerName%3Dsleepy-pod-inf%0AImage%3Dquay.io%2Ffedora%2Ffedora%0AExec%3Dsleep%20infinity%0A%5BInstall%5D%0AWantedBy%3Dmulti-user.target" + + SleepContainerTemplate = `[Unit] +Description=A templated sleepy container +[Container] +Image=quay.io/fedora/fedora +Exec=sleep %i +[Service] +# Restart service when sleep finishes +Restart=always +[Install] +WantedBy=multi-user.target` + + SleepContainerTemplateAsData = "data:;base64,H4sIAAAAAAAC/zSNvU4DMRCE+32KlRAl4Qlc8FekBSGKk4uVb5Ks5Fsf3j2C3x6JJNWMRvPpmz5NI9MrvHRdQ5ulJw4sa5XAzF6BdXBpFqKGTtPLrWbaL3JE+t5k7LQ9HjC3Ltegt1+U9E/zvdL0gf6jBZnu+B0e0oP9MvH5BLt4+KCmfoLT9ZOknmU4TXvzkFozfYkF5ueRlq2GPmyOvgvpR8RfAAAA//91kIIEyQAAAA==" + ) + + filesDir := t.TempDir() + fileContents := map[string]string{ + "sample.container": SleepContainer, + "sample@.container": SleepContainerTemplate, + } + for name, contents := range fileContents { + if err := os.MkdirAll(filepath.Join(filesDir, filepath.Dir(name)), 0755); err != nil { + t.Error(err) + return + } + err := os.WriteFile(filepath.Join(filesDir, name), []byte(contents), 0644) + if err != nil { + t.Error(err) + return + } + } + + tests := []struct { + name string + inputConfig Config + outConf types.Config + reportPath string + options common.TranslateOptions + }{ + { + name: "Basic .container quadlets", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: true, + }, + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: false, + }, + }, + }, + }, + outConf: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(SleepContainerAsData), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { + Node: types.Node{ + Path: "/etc/containers/systemd/users/sleepy.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(SleepContainerAsData), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + }, + }, + }, + { + name: "Template instance is symlink", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy@.container", + Contents: util.StrToPtr(SleepContainerTemplate), + Rootful: true, + }, + { + Name: "sleepy@100.container", + Rootful: true, + }, + }, + }, + }, + outConf: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(SleepContainerTemplateAsData), + Compression: util.StrToPtr("gzip"), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + Links: []types.Link{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@100.container", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("sleepy@.container"), + }, + }, + }, + }, + }, + }, + { + name: "Quadlet with non-existent contents_local", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy.container", + ContentsLocal: util.StrToPtr("fake-file.container"), + Rootful: true, + }, + }, + }, + }, + options: common.TranslateOptions{FilesDir: filesDir}, + reportPath: "error at $.systemd.quadlets.0.contents_local: open " + filepath.Join(filesDir, "fake-file.container") + ": " + osNotFound + "\n", + }, + { + name: "Quadlet with contents_local", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy.container", + ContentsLocal: util.StrToPtr("sample.container"), + Rootful: true, + }, + }, + }, + }, + outConf: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(SleepContainerAsData), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + }, + }, + options: common.TranslateOptions{FilesDir: filesDir}, + }, + { + name: "Overrides break", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + // two quadlets with the same name should give an error + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainer), + Rootful: true, + }, + { + Name: "sleepy.container", + Contents: util.StrToPtr(SleepContainerTemplate), + Rootful: true, + }, + }, + }, + }, + outConf: types.Config{}, + reportPath: "error at $.systemd.quadlets.1: matching filesystem node has existing contents or different type\n", + }, + + { + name: "Template with dropin", + inputConfig: Config{ + Systemd: Systemd{ + Quadlets: []Quadlet{ + { + Name: "sleepy@.container", + Contents: util.StrToPtr(SleepContainerTemplate), + Rootful: true, + Dropins: []Dropin{ + { + Name: "sample.conf", + Contents: util.StrToPtr("[Service]\nTimeoutStartSec=900"), + }, + }, + }, + { + Name: "sleepy@100.container", + Rootful: true, + Dropins: []Dropin{ + { + Name: "foo.conf", + Contents: util.StrToPtr("[Service]\nTimeoutStartSec=900"), + }, + }, + }, + }, + }, + }, + outConf: types.Config{ + Ignition: types.Ignition{ + Version: "3.7.0-experimental", + }, + Storage: types.Storage{ + Files: []types.File{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@.container", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr(SleepContainerTemplateAsData), + Compression: util.StrToPtr("gzip"), + }, + Mode: util.IntToPtr(0644), + }, + }, + { // Dropin for all sleepy@.container instances + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@.container.d/sample.conf", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,%5BService%5D%0ATimeoutStartSec%3D900"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + { // Dropin for the instance of sleepy@.container with 100 as input + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@100.container.d/foo.conf", + }, + FileEmbedded1: types.FileEmbedded1{ + Contents: types.Resource{ + Source: util.StrToPtr("data:,%5BService%5D%0ATimeoutStartSec%3D900"), + Compression: util.StrToPtr(""), + }, + Mode: util.IntToPtr(0644), + }, + }, + }, + Links: []types.Link{ + { + Node: types.Node{ + Path: "/etc/containers/systemd/sleepy@100.container", + }, + LinkEmbedded1: types.LinkEmbedded1{ + Target: util.StrToPtr("sleepy@.container"), + }, + }, + }, + }, + }, + options: common.TranslateOptions{FilesDir: filesDir}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + c, _, r := test.inputConfig.ToIgn3_7Unvalidated(test.options) + assert.Equal(t, test.outConf, c, "translation mismatch") + assert.Equal(t, test.reportPath, r.String(), "report mismatch") + }) + } +} diff --git a/base/v0_8_exp/validate.go b/base/v0_8_exp/validate.go index 4b37c8f1..806ee493 100644 --- a/base/v0_8_exp/validate.go +++ b/base/v0_8_exp/validate.go @@ -91,16 +91,54 @@ func (t Tree) Validate(c path.ContextPath) (r report.Report) { return } -func (rs Unit) Validate(c path.ContextPath) (r report.Report) { - if rs.ContentsLocal != nil && rs.Contents != nil { +func validateNotTooManySources(contentsLocal, contents *string, c path.ContextPath) (r report.Report) { + if contentsLocal != nil && contents != nil { r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) } return } +func (rs Unit) Validate(c path.ContextPath) (r report.Report) { + return validateNotTooManySources(rs.ContentsLocal, rs.Contents, c) +} + func (rs Dropin) Validate(c path.ContextPath) (r report.Report) { - if rs.ContentsLocal != nil && rs.Contents != nil { - r.AddOnError(c.Append("contents_local"), common.ErrTooManySystemdSources) + return validateNotTooManySources(rs.ContentsLocal, rs.Contents, c) +} + +// All accepted extensions by podman-systemd.unit +func quadletExtension(name string) error { + extensionIsSupported := strings.HasSuffix(name, ".container") || + strings.HasSuffix(name, ".volume") || + strings.HasSuffix(name, ".network") || + strings.HasSuffix(name, ".kube") || + strings.HasSuffix(name, ".image") || + strings.HasSuffix(name, ".build") || + strings.HasSuffix(name, ".pod") || + strings.HasSuffix(name, ".artifact") + + if !extensionIsSupported { + return common.ErrQuadletBadExtension } - return + + return nil +} + +// Validate checks the quadlet name has a valid extension and template instances don't have contents. +func (rs Quadlet) Validate(c path.ContextPath) (r report.Report) { + r = validateNotTooManySources(rs.ContentsLocal, rs.Contents, c) + // Template instances cannot have a content as they are symlinks + if isTemplateInstance, _ := isTemplateInstance(rs.Name); isTemplateInstance { + if rs.Contents != nil { + contentPath := c.Append("contents") + r.AddOnError(contentPath, common.ErrTemplateInstanceCannotHaveContents) + } else if rs.ContentsLocal != nil { + contentPath := c.Append("contents_local") + r.AddOnError(contentPath, common.ErrTemplateInstanceCannotHaveContents) + } + } + if err := quadletExtension(rs.Name); err != nil { + r.AddOnError(c.Append("name"), err) + } + return r } diff --git a/base/v0_8_exp/validate_test.go b/base/v0_8_exp/validate_test.go index e05a7de4..19f37550 100644 --- a/base/v0_8_exp/validate_test.go +++ b/base/v0_8_exp/validate_test.go @@ -388,6 +388,98 @@ func TestValidateDropin(t *testing.T) { } } +func TestValidateQuadlet(t *testing.T) { + tests := []struct { + in Quadlet + out error + errPath path.ContextPath + }{ + { + Quadlet{ + Name: "working.container", + ContentsLocal: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + { + Quadlet{ + Name: "working.container", + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + { + Quadlet{ + Name: "bad-extension.foo", + Contents: util.StrToPtr("hello"), + }, + common.ErrQuadletBadExtension, + path.New("yaml", "name"), + }, + { + Quadlet{ + Name: "testing.container", + Contents: util.StrToPtr("hello"), + ContentsLocal: util.StrToPtr("hello"), + }, + common.ErrTooManySystemdSources, + path.New("yaml", "contents_local"), + }, + // No contents and no contents_local is allowed + { + Quadlet{ + Name: "testing.container", + }, + nil, + path.New("yaml"), + }, + { + Quadlet{ + Name: "templateBase@.container", + Contents: util.StrToPtr("hello"), + }, + nil, + path.New("yaml"), + }, + // template instance cannot have contents + { + Quadlet{ + Name: "templateInstance@1000.container", + }, + nil, + path.New("yaml"), + }, + { + Quadlet{ + Name: "templateInstance@1000.container", + ContentsLocal: util.StrToPtr("hello"), + }, + common.ErrTemplateInstanceCannotHaveContents, + path.New("yaml", "contents_local"), + }, + { + Quadlet{ + Name: "templateInstance@1000.container", + Contents: util.StrToPtr("hello"), + }, + common.ErrTemplateInstanceCannotHaveContents, + path.New("yaml", "contents"), + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("validate %d", i), func(t *testing.T) { + actual := test.in.Validate(path.New("yaml")) + baseutil.VerifyReport(t, test.in, actual) + expected := report.Report{} + expected.AddOnError(test.errPath, test.out) + assert.Equal(t, expected, actual, "bad report") + }) + } +} + // TestUnkownIgnitionVersion tests that butane will raise a warning but will not fail when an ignition config with an unkown version is specified func TestUnkownIgnitionVersion(t *testing.T) { test := struct { diff --git a/config/common/errors.go b/config/common/errors.go index 0f82b400..d3b686db 100644 --- a/config/common/errors.go +++ b/config/common/errors.go @@ -46,7 +46,9 @@ var ( ErrDecimalMode = errors.New("unreasonable mode would be reasonable if specified in octal; remember to add a leading zero") // systemd - ErrTooManySystemdSources = errors.New("only one of the following can be set: contents, contents_local") + ErrTooManySystemdSources = errors.New("only one of the following can be set: contents, contents_local") + ErrQuadletBadExtension = errors.New("unsupported file extension for quadlet: must be one of .container, .volume, .network, .kube, .image, .build, .pod, or .artifact") + ErrTemplateInstanceCannotHaveContents = errors.New("template instances cannot have contents or contents_local") // mount units ErrMountUnitNoPath = errors.New("path is required if with_mount_unit is true and format is not swap") diff --git a/docs/config-fcos-v1_8-exp.md b/docs/config-fcos-v1_8-exp.md index dc79f7e2..133c0d3a 100644 --- a/docs/config-fcos-v1_8-exp.md +++ b/docs/config-fcos-v1_8-exp.md @@ -192,6 +192,15 @@ The Fedora CoreOS configuration is a YAML document conforming to the following s * **name** (string): the name of the drop-in. This must be suffixed with ".conf". * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as rootful. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. diff --git a/docs/config-fiot-v1_1-exp.md b/docs/config-fiot-v1_1-exp.md index e6f680c5..e7dbc636 100644 --- a/docs/config-fiot-v1_1-exp.md +++ b/docs/config-fiot-v1_1-exp.md @@ -131,6 +131,15 @@ The Fedora IoT configuration is a YAML document conforming to the following spec * **name** (string): the name of the drop-in. This must be suffixed with ".conf". * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as rootful. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. diff --git a/docs/config-flatcar-v1_2-exp.md b/docs/config-flatcar-v1_2-exp.md index 4eb2ea7c..80a52faf 100644 --- a/docs/config-flatcar-v1_2-exp.md +++ b/docs/config-flatcar-v1_2-exp.md @@ -190,6 +190,15 @@ The Flatcar configuration is a YAML document conforming to the following specifi * **name** (string): the name of the drop-in. This must be suffixed with ".conf". * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as rootful. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. diff --git a/docs/config-openshift-v4_22-exp.md b/docs/config-openshift-v4_22-exp.md index 577363de..7c5ccbde 100644 --- a/docs/config-openshift-v4_22-exp.md +++ b/docs/config-openshift-v4_22-exp.md @@ -161,6 +161,15 @@ The OpenShift configuration is a YAML document conforming to the following speci * **name** (string): the name of the drop-in. This must be suffixed with ".conf". * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as rootful. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. Must be `core`. diff --git a/docs/config-r4e-v1_2-exp.md b/docs/config-r4e-v1_2-exp.md index cae583cd..0416c394 100644 --- a/docs/config-r4e-v1_2-exp.md +++ b/docs/config-r4e-v1_2-exp.md @@ -131,6 +131,15 @@ The RHEL for Edge configuration is a YAML document conforming to the following s * **name** (string): the name of the drop-in. This must be suffixed with ".conf". * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_quadlets_** (list of objects): a list of Podman Quadlet files. + * **name** (string): the name of the quadlet file. + * **rootful** (boolean): whether the quadlet runs as rootful. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`. Defaults to false. + * **_contents_** (string): the contents of the quadlet file. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + * **_dropins_** (list of objects): drop-ins to override settings in the quadlet. + * **name** (string): the name of the drop-in file. + * **_contents_** (string): the contents of the drop-in. Mutually exclusive with `contents_local`. + * **_contents_local_** (string): a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. * **_passwd_** (object): describes the desired additions to the passwd database. * **_users_** (list of objects): the list of accounts that shall exist. All users must have a unique `name`. * **name** (string): the username for the account. diff --git a/docs/release-notes.md b/docs/release-notes.md index 0145fbd8..9d47929d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,10 @@ nav_order: 9 ### Features +- Add `systemd.quadlets` section for embedding Podman Quadlet files + _(fcos 1.8.0-exp, fiot 1.1.0-exp, flatcar 1.2.0-exp, openshift + 4.22.0-exp, r4e 1.2.0-exp)_ + ### Bug fixes ### Misc. changes diff --git a/internal/doc/butane.yaml b/internal/doc/butane.yaml index 926853d1..db1ff46b 100644 --- a/internal/doc/butane.yaml +++ b/internal/doc/butane.yaml @@ -340,6 +340,27 @@ root: - name: contents_local after: contents desc: a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + - name: quadlets + after: units + desc: a list of Podman Quadlet files. + children: + - name: name + desc: the name of the quadlet file. + - name: rootful + desc: whether the quadlet runs as rootful. If true, the file is placed in `/etc/containers/systemd`. If false, it is placed in `/etc/containers/systemd/users`. Defaults to false. + - name: contents + desc: the contents of the quadlet file. Mutually exclusive with `contents_local`. + - name: contents_local + desc: a local path to the contents of the quadlet file, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. + - name: dropins + desc: drop-ins to override settings in the quadlet. + children: + - name: name + desc: the name of the drop-in file. + - name: contents + desc: the contents of the drop-in. Mutually exclusive with `contents_local`. + - name: contents_local + desc: a local path to the contents of the drop-in, relative to the directory specified by the `--files-dir` command-line argument. Mutually exclusive with `contents`. - name: passwd children: - name: users