mirror of https://github.com/containers/podman.git
Merge 40aaae18b5
into dfd205fa24
This commit is contained in:
commit
0fccfb7d8d
|
@ -173,6 +173,28 @@ func getPods(cmd *cobra.Command, toComplete string, cType completeType, statuses
|
||||||
return suggestions, cobra.ShellCompDirectiveNoFileComp
|
return suggestions, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getQuadlets(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
suggestions := []string{}
|
||||||
|
lsOpts := entities.QuadletListOptions{}
|
||||||
|
engine, err := setupContainerEngine(cmd)
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
quadlets, err := engine.QuadletList(registry.Context(), lsOpts)
|
||||||
|
if err != nil {
|
||||||
|
cobra.CompErrorln(err.Error())
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, q := range quadlets {
|
||||||
|
if strings.HasPrefix(q.Name, toComplete) {
|
||||||
|
suggestions = append(suggestions, q.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return suggestions, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
|
||||||
func getVolumes(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) {
|
func getVolumes(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
suggestions := []string{}
|
suggestions := []string{}
|
||||||
lsOpts := entities.VolumeListOptions{}
|
lsOpts := entities.VolumeListOptions{}
|
||||||
|
@ -730,6 +752,14 @@ func AutocompleteImages(cmd *cobra.Command, args []string, toComplete string) ([
|
||||||
return getImages(cmd, toComplete)
|
return getImages(cmd, toComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AutocompleteQuadlets - Autocomplete quadlets.
|
||||||
|
func AutocompleteQuadlets(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if !validCurrentCmdLine(cmd, args, toComplete) {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getQuadlets(cmd, toComplete)
|
||||||
|
}
|
||||||
|
|
||||||
// AutocompleteManifestListAndMember - Autocomplete names of manifest lists and digests of items in them.
|
// AutocompleteManifestListAndMember - Autocomplete names of manifest lists and digests of items in them.
|
||||||
func AutocompleteManifestListAndMember(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
func AutocompleteManifestListAndMember(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if !validCurrentCmdLine(cmd, args, toComplete) {
|
if !validCurrentCmdLine(cmd, args, toComplete) {
|
||||||
|
@ -827,6 +857,11 @@ func AutocompleteDefaultOneArg(cmd *cobra.Command, args []string, toComplete str
|
||||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AutocompleteDefaultManyArg - Autocomplete for many args.
|
||||||
|
func AutocompleteDefaultManyArg(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return nil, cobra.ShellCompDirectiveDefault
|
||||||
|
}
|
||||||
|
|
||||||
// AutocompleteCommitCommand - Autocomplete podman commit command args.
|
// AutocompleteCommitCommand - Autocomplete podman commit command args.
|
||||||
func AutocompleteCommitCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
func AutocompleteCommitCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
if !validCurrentCmdLine(cmd, args, toComplete) {
|
if !validCurrentCmdLine(cmd, args, toComplete) {
|
||||||
|
@ -1775,6 +1810,14 @@ func AutocompletePsFilters(cmd *cobra.Command, args []string, toComplete string)
|
||||||
return completeKeyValues(toComplete, kv)
|
return completeKeyValues(toComplete, kv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AutocompleteQuadletFilters - Autocomplete quadlet filter options.
|
||||||
|
func AutocompleteQuadletFilters(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
kv := keyValueCompletion{
|
||||||
|
"name=": func(s string) ([]string, cobra.ShellCompDirective) { return getQuadlets(cmd, s) },
|
||||||
|
}
|
||||||
|
return completeKeyValues(toComplete, kv)
|
||||||
|
}
|
||||||
|
|
||||||
// AutocompletePodPsFilters - Autocomplete pod ps filter options.
|
// AutocompletePodPsFilters - Autocomplete pod ps filter options.
|
||||||
func AutocompletePodPsFilters(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
func AutocompletePodPsFilters(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
kv := keyValueCompletion{
|
kv := keyValueCompletion{
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
_ "github.com/containers/podman/v5/cmd/podman/manifest"
|
_ "github.com/containers/podman/v5/cmd/podman/manifest"
|
||||||
_ "github.com/containers/podman/v5/cmd/podman/networks"
|
_ "github.com/containers/podman/v5/cmd/podman/networks"
|
||||||
_ "github.com/containers/podman/v5/cmd/podman/pods"
|
_ "github.com/containers/podman/v5/cmd/podman/pods"
|
||||||
|
_ "github.com/containers/podman/v5/cmd/podman/quadlet"
|
||||||
"github.com/containers/podman/v5/cmd/podman/registry"
|
"github.com/containers/podman/v5/cmd/podman/registry"
|
||||||
_ "github.com/containers/podman/v5/cmd/podman/secrets"
|
_ "github.com/containers/podman/v5/cmd/podman/secrets"
|
||||||
_ "github.com/containers/podman/v5/cmd/podman/system"
|
_ "github.com/containers/podman/v5/cmd/podman/system"
|
||||||
|
@ -26,6 +27,7 @@ import (
|
||||||
"github.com/containers/podman/v5/cmd/podman/validate"
|
"github.com/containers/podman/v5/cmd/podman/validate"
|
||||||
_ "github.com/containers/podman/v5/cmd/podman/volumes"
|
_ "github.com/containers/podman/v5/cmd/podman/volumes"
|
||||||
"github.com/containers/podman/v5/pkg/domain/entities"
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||||
|
"github.com/containers/podman/v5/pkg/logiface"
|
||||||
"github.com/containers/podman/v5/pkg/rootless"
|
"github.com/containers/podman/v5/pkg/rootless"
|
||||||
"github.com/containers/podman/v5/pkg/terminal"
|
"github.com/containers/podman/v5/pkg/terminal"
|
||||||
"github.com/containers/storage/pkg/reexec"
|
"github.com/containers/storage/pkg/reexec"
|
||||||
|
@ -34,12 +36,22 @@ import (
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type logrusLogger struct{}
|
||||||
|
|
||||||
|
func (l logrusLogger) Errorf(format string, args ...interface{}) {
|
||||||
|
logrus.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
func (l logrusLogger) Debugf(format string, args ...interface{}) {
|
||||||
|
logrus.Debugf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if reexec.Init() {
|
if reexec.Init() {
|
||||||
// We were invoked with a different argv[0] indicating that we
|
// We were invoked with a different argv[0] indicating that we
|
||||||
// had a specific job to do as a subprocess, and it's done.
|
// had a specific job to do as a subprocess, and it's done.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
logiface.SetLogger(logrusLogger{})
|
||||||
|
|
||||||
if filepath.Base(os.Args[0]) == registry.PodmanSh ||
|
if filepath.Base(os.Args[0]) == registry.PodmanSh ||
|
||||||
(len(os.Args[0]) > 0 && filepath.Base(os.Args[0][1:]) == registry.PodmanSh) {
|
(len(os.Args[0]) > 0 && filepath.Base(os.Args[0][1:]) == registry.PodmanSh) {
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package quadlet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/common"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/registry"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/utils"
|
||||||
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quadletInstallDescription = `Install one or more Quadlets for the current user. Quadlets may be specified as local files, Web URLs, and OCI artifacts.`
|
||||||
|
|
||||||
|
quadletInstallCmd = &cobra.Command{
|
||||||
|
Use: "install [options] PATH-OR-URL [PATH-OR-URL...]",
|
||||||
|
Short: "Install one or more quadlets",
|
||||||
|
Long: quadletInstallDescription,
|
||||||
|
RunE: install,
|
||||||
|
Args: func(_ *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return fmt.Errorf("must provide at least one argument")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ValidArgsFunction: common.AutocompleteDefaultManyArg,
|
||||||
|
Example: `podman quadlet install /path/to/myquadlet.container
|
||||||
|
podman quadlet install https://github.com/containers/podman/blob/main/test/e2e/quadlet/basic.container
|
||||||
|
podman quadlet install oci-artifact://my-artifact:latest`,
|
||||||
|
}
|
||||||
|
|
||||||
|
installOptions entities.QuadletInstallOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
func installFlags(cmd *cobra.Command) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVar(&installOptions.ReloadSystemd, "reload-systemd", true, "Reload systemd after installing Quadlets")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||||
|
Command: quadletInstallCmd,
|
||||||
|
Parent: quadletCmd,
|
||||||
|
})
|
||||||
|
installFlags(quadletInstallCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func install(cmd *cobra.Command, args []string) error {
|
||||||
|
var errs utils.OutputErrors
|
||||||
|
installReport, err := registry.ContainerEngine().QuadletInstall(registry.Context(), args, installOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for pathOrURL, err := range installReport.QuadletErrors {
|
||||||
|
errs = append(errs, fmt.Errorf("quadlet %q failed to install: %v", pathOrURL, err))
|
||||||
|
}
|
||||||
|
for _, s := range installReport.InstalledQuadlets {
|
||||||
|
fmt.Println(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(installReport.QuadletErrors) > 0 {
|
||||||
|
errs = append(errs, errors.New("errors occurred installing some Quadlets"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs.PrintErrors()
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package quadlet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/containers/common/pkg/completion"
|
||||||
|
"github.com/containers/common/pkg/report"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/common"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/registry"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/validate"
|
||||||
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quadletListDescription = `List all Quadlets configured for the current user.`
|
||||||
|
|
||||||
|
quadletListCmd = &cobra.Command{
|
||||||
|
Use: "list [options]",
|
||||||
|
Short: "List Quadlets",
|
||||||
|
Long: quadletListDescription,
|
||||||
|
RunE: list,
|
||||||
|
Args: validate.NoArgs,
|
||||||
|
ValidArgsFunction: completion.AutocompleteNone,
|
||||||
|
Example: `podman quadlet list
|
||||||
|
podman quadlet list --format '{{ .Unit }}'
|
||||||
|
podman quadlet list --filter 'name=test*'`,
|
||||||
|
}
|
||||||
|
|
||||||
|
listOptions entities.QuadletListOptions
|
||||||
|
format string
|
||||||
|
)
|
||||||
|
|
||||||
|
func listFlags(cmd *cobra.Command) {
|
||||||
|
formatFlagName := "format"
|
||||||
|
filterFlagName := "filter"
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.StringArrayVarP(&listOptions.Filters, filterFlagName, "f", []string{}, "Filter output based on conditions given")
|
||||||
|
flags.StringVar(&format, formatFlagName, "{{range .}}{{.Name}}\t{{.UnitName}}\t{{.Path}}\t{{.Status}}\n{{end -}}", "Pretty-print output to JSON or using a Go template")
|
||||||
|
_ = quadletListCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&entities.ListQuadlet{}))
|
||||||
|
_ = quadletListCmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompleteQuadletFilters)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||||
|
Command: quadletListCmd,
|
||||||
|
Parent: quadletCmd,
|
||||||
|
})
|
||||||
|
listFlags(quadletListCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func list(cmd *cobra.Command, args []string) error {
|
||||||
|
quadlets, err := registry.ContainerEngine().QuadletList(registry.Context(), listOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if report.IsJSON(format) {
|
||||||
|
return outputJSON(quadlets)
|
||||||
|
}
|
||||||
|
return outputTemplate(cmd, quadlets)
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputTemplate(cmd *cobra.Command, responses []*entities.ListQuadlet) error {
|
||||||
|
headers := report.Headers(entities.ListQuadlet{}, map[string]string{
|
||||||
|
"Name": "NAME",
|
||||||
|
"UnitName": "UNIT NAME",
|
||||||
|
"Path": "PATH ON DISK",
|
||||||
|
"Status": "STATUS",
|
||||||
|
})
|
||||||
|
|
||||||
|
rpt := report.New(os.Stdout, cmd.Name())
|
||||||
|
defer rpt.Flush()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch {
|
||||||
|
case cmd.Flag("format").Changed:
|
||||||
|
rpt, err = rpt.Parse(report.OriginUser, format)
|
||||||
|
default:
|
||||||
|
rpt, err = rpt.Parse(report.OriginPodman, format)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rpt.Execute(headers); err != nil {
|
||||||
|
return fmt.Errorf("writing column headers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpt.Execute(responses)
|
||||||
|
}
|
||||||
|
|
||||||
|
func outputJSON(vols []*entities.ListQuadlet) error {
|
||||||
|
b, err := json.MarshalIndent(vols, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(string(b))
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package quadlet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/common"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/registry"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quadletPrintDescription = `Print the contents of a Quadlet, displaying the file including all comments`
|
||||||
|
|
||||||
|
quadletPrintCmd = &cobra.Command{
|
||||||
|
Use: "print QUADLET",
|
||||||
|
Short: "Display the contents of a quadlet",
|
||||||
|
Long: quadletPrintDescription,
|
||||||
|
RunE: print,
|
||||||
|
ValidArgsFunction: common.AutocompleteQuadlets,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Example: `podman quadlet print myquadlet.container
|
||||||
|
podman quadlet print mypod.pod
|
||||||
|
podman quadlet print myimage.build`,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||||
|
Command: quadletPrintCmd,
|
||||||
|
Parent: quadletCmd,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func print(cmd *cobra.Command, args []string) error {
|
||||||
|
quadletContents, err := registry.ContainerEngine().QuadletPrint(registry.Context(), args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print(quadletContents)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package quadlet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/registry"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/validate"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Pull in configured json library
|
||||||
|
json = registry.JSONLibrary()
|
||||||
|
|
||||||
|
// Command: podman _quadlet_
|
||||||
|
quadletCmd = &cobra.Command{
|
||||||
|
Use: "quadlet",
|
||||||
|
Short: "Allows users to manage Quadlets",
|
||||||
|
Long: "Allows users to manage Quadlets",
|
||||||
|
RunE: validate.SubCommandExists,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||||
|
Command: quadletCmd,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package quadlet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/common"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/registry"
|
||||||
|
"github.com/containers/podman/v5/cmd/podman/utils"
|
||||||
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quadletRmDescription = `Remove one or more installed Quadlets from the current user`
|
||||||
|
|
||||||
|
quadletRmCmd = &cobra.Command{
|
||||||
|
Use: "rm [options] QUADLET [QUADLET...]",
|
||||||
|
Short: "Remove Quadlets",
|
||||||
|
Long: quadletRmDescription,
|
||||||
|
RunE: rm,
|
||||||
|
ValidArgsFunction: common.AutocompleteQuadlets,
|
||||||
|
Example: `podman quadlet rm test.container
|
||||||
|
podman quadlet rm --force mysql.container
|
||||||
|
podman quadlet rm --all --reload-systemd=false`,
|
||||||
|
}
|
||||||
|
|
||||||
|
removeOptions entities.QuadletRemoveOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
func rmFlags(cmd *cobra.Command) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
flags.BoolVarP(&removeOptions.Force, "force", "f", false, "Remove running quadlets")
|
||||||
|
flags.BoolVarP(&removeOptions.All, "all", "a", false, "Remove all Quadlets for the current user")
|
||||||
|
flags.BoolVarP(&removeOptions.Ignore, "ignore", "i", false, "Do not error for Quadlets that do not exist")
|
||||||
|
flags.BoolVar(&removeOptions.ReloadSystemd, "reload-systemd", false, "Reload systemd after removal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.Commands = append(registry.Commands, registry.CliCommand{
|
||||||
|
Command: quadletRmCmd,
|
||||||
|
Parent: quadletCmd,
|
||||||
|
})
|
||||||
|
rmFlags(quadletRmCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rm(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) < 1 && !removeOptions.All {
|
||||||
|
return errors.New("at least one quadlet file must be selected")
|
||||||
|
}
|
||||||
|
var errs utils.OutputErrors
|
||||||
|
removeReport, err := registry.ContainerEngine().QuadletRemove(registry.Context(), args, removeOptions)
|
||||||
|
// We can get a report back even if err != nil if systemd reload failed
|
||||||
|
if removeReport != nil {
|
||||||
|
for _, rq := range removeReport.Removed {
|
||||||
|
fmt.Println(rq)
|
||||||
|
}
|
||||||
|
for quadlet, quadletErr := range removeReport.Errors {
|
||||||
|
errs = append(errs, fmt.Errorf("unable to remove Quadlet %s: %v", quadlet, quadletErr))
|
||||||
|
}
|
||||||
|
if err == nil && len(removeReport.Errors) > 0 {
|
||||||
|
errs = append(errs, errors.New("some quadlets could not be removed"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs.PrintErrors()
|
||||||
|
}
|
|
@ -4,12 +4,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
@ -92,219 +89,6 @@ func Debugf(format string, a ...interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type searchPaths struct {
|
|
||||||
sorted []string
|
|
||||||
// map to store paths so we can quickly check if we saw them already and not loop in case of symlinks
|
|
||||||
visitedDirs map[string]struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSearchPaths() *searchPaths {
|
|
||||||
return &searchPaths{
|
|
||||||
sorted: make([]string, 0),
|
|
||||||
visitedDirs: make(map[string]struct{}, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *searchPaths) Add(path string) {
|
|
||||||
s.sorted = append(s.sorted, path)
|
|
||||||
s.visitedDirs[path] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *searchPaths) Visited(path string) bool {
|
|
||||||
_, visited := s.visitedDirs[path]
|
|
||||||
return visited
|
|
||||||
}
|
|
||||||
|
|
||||||
// This returns the directories where we read quadlet .container and .volumes from
|
|
||||||
// For system generators these are in /usr/share/containers/systemd (for distro files)
|
|
||||||
// and /etc/containers/systemd (for sysadmin files).
|
|
||||||
// For user generators these can live in $XDG_RUNTIME_DIR/containers/systemd, /etc/containers/systemd/users, /etc/containers/systemd/users/$UID, and $XDG_CONFIG_HOME/containers/systemd
|
|
||||||
func getUnitDirs(rootless bool) []string {
|
|
||||||
paths := newSearchPaths()
|
|
||||||
|
|
||||||
// Allow overriding source dir, this is mainly for the CI tests
|
|
||||||
if getDirsFromEnv(paths) {
|
|
||||||
return paths.sorted
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedUnitDirAdminUser := resolveUnitDirAdminUser()
|
|
||||||
userLevelFilter := getUserLevelFilter(resolvedUnitDirAdminUser)
|
|
||||||
|
|
||||||
if rootless {
|
|
||||||
systemUserDirLevel := len(strings.Split(resolvedUnitDirAdminUser, string(os.PathSeparator)))
|
|
||||||
nonNumericFilter := getNonNumericFilter(resolvedUnitDirAdminUser, systemUserDirLevel)
|
|
||||||
getRootlessDirs(paths, nonNumericFilter, userLevelFilter)
|
|
||||||
} else {
|
|
||||||
getRootDirs(paths, userLevelFilter)
|
|
||||||
}
|
|
||||||
return paths.sorted
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDirsFromEnv(paths *searchPaths) bool {
|
|
||||||
unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS")
|
|
||||||
if len(unitDirsEnv) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, eachUnitDir := range strings.Split(unitDirsEnv, ":") {
|
|
||||||
if !filepath.IsAbs(eachUnitDir) {
|
|
||||||
Logf("%s not a valid file path", eachUnitDir)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
appendSubPaths(paths, eachUnitDir, false, nil)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRootlessDirs(paths *searchPaths, nonNumericFilter, userLevelFilter func(string, bool) bool) {
|
|
||||||
runtimeDir, found := os.LookupEnv("XDG_RUNTIME_DIR")
|
|
||||||
if found {
|
|
||||||
appendSubPaths(paths, path.Join(runtimeDir, "containers/systemd"), false, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
configDir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
appendSubPaths(paths, path.Join(configDir, "containers/systemd"), false, nil)
|
|
||||||
|
|
||||||
u, err := user.Current()
|
|
||||||
if err == nil {
|
|
||||||
appendSubPaths(paths, filepath.Join(quadlet.UnitDirAdmin, "users"), true, nonNumericFilter)
|
|
||||||
appendSubPaths(paths, filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid), true, userLevelFilter)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
|
||||||
// Add the base directory even if the UID was not found
|
|
||||||
paths.Add(filepath.Join(quadlet.UnitDirAdmin, "users"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRootDirs(paths *searchPaths, userLevelFilter func(string, bool) bool) {
|
|
||||||
appendSubPaths(paths, quadlet.UnitDirTemp, false, userLevelFilter)
|
|
||||||
appendSubPaths(paths, quadlet.UnitDirAdmin, false, userLevelFilter)
|
|
||||||
appendSubPaths(paths, quadlet.UnitDirDistro, false, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveUnitDirAdminUser() string {
|
|
||||||
unitDirAdminUser := filepath.Join(quadlet.UnitDirAdmin, "users")
|
|
||||||
var err error
|
|
||||||
var resolvedUnitDirAdminUser string
|
|
||||||
if resolvedUnitDirAdminUser, err = filepath.EvalSymlinks(unitDirAdminUser); err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
Debugf("Error occurred resolving path %q: %s", unitDirAdminUser, err)
|
|
||||||
}
|
|
||||||
resolvedUnitDirAdminUser = unitDirAdminUser
|
|
||||||
}
|
|
||||||
return resolvedUnitDirAdminUser
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendSubPaths(paths *searchPaths, path string, isUserFlag bool, filterPtr func(string, bool) bool) {
|
|
||||||
resolvedPath, err := filepath.EvalSymlinks(path)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
Debugf("Error occurred resolving path %q: %s", path, err)
|
|
||||||
}
|
|
||||||
// Despite the failure add the path to the list for logging purposes
|
|
||||||
// This is the equivalent of adding the path when info==nil below
|
|
||||||
paths.Add(path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if skipPath(paths, resolvedPath, isUserFlag, filterPtr) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the current directory
|
|
||||||
paths.Add(resolvedPath)
|
|
||||||
|
|
||||||
// Read the contents of the directory
|
|
||||||
entries, err := os.ReadDir(resolvedPath)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
Debugf("Error occurred walking sub directories %q: %s", path, err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively run through the contents of the directory
|
|
||||||
for _, entry := range entries {
|
|
||||||
fullPath := filepath.Join(resolvedPath, entry.Name())
|
|
||||||
appendSubPaths(paths, fullPath, isUserFlag, filterPtr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipPath(paths *searchPaths, path string, isUserFlag bool, filterPtr func(string, bool) bool) bool {
|
|
||||||
// If the path is already in the map no need to read it again
|
|
||||||
if paths.Visited(path) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't traverse drop-in directories
|
|
||||||
if strings.HasSuffix(path, ".d") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the directory should be filtered out
|
|
||||||
if filterPtr != nil && !filterPtr(path, isUserFlag) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
stat, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
|
||||||
Debugf("Error occurred resolving path %q: %s", path, err)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not a directory nothing to add
|
|
||||||
return !stat.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNonNumericFilter(resolvedUnitDirAdminUser string, systemUserDirLevel int) func(string, bool) bool {
|
|
||||||
return func(path string, isUserFlag bool) bool {
|
|
||||||
// when running in rootless, recursive walk directories that are non numeric
|
|
||||||
// ignore sub dirs under the `users` directory which correspond to a user id
|
|
||||||
if strings.HasPrefix(path, resolvedUnitDirAdminUser) {
|
|
||||||
listDirUserPathLevels := strings.Split(path, string(os.PathSeparator))
|
|
||||||
// Make sure to add the base directory
|
|
||||||
if len(listDirUserPathLevels) == systemUserDirLevel {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(listDirUserPathLevels) > systemUserDirLevel {
|
|
||||||
if !(regexp.MustCompile(`^[0-9]*$`).MatchString(listDirUserPathLevels[systemUserDirLevel])) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUserLevelFilter(resolvedUnitDirAdminUser string) func(string, bool) bool {
|
|
||||||
return func(_path string, isUserFlag bool) bool {
|
|
||||||
// if quadlet generator is run rootless, do not recurse other user sub dirs
|
|
||||||
// if quadlet generator is run as root, ignore users sub dirs
|
|
||||||
if strings.HasPrefix(_path, resolvedUnitDirAdminUser) {
|
|
||||||
if isUserFlag {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isExtSupported(filename string) bool {
|
|
||||||
ext := filepath.Ext(filename)
|
|
||||||
_, ok := quadlet.SupportedExtensions[ext]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
var seen = make(map[string]struct{})
|
var seen = make(map[string]struct{})
|
||||||
|
|
||||||
func loadUnitsFromDir(sourcePath string) ([]*parser.UnitFile, error) {
|
func loadUnitsFromDir(sourcePath string) ([]*parser.UnitFile, error) {
|
||||||
|
@ -321,7 +105,7 @@ func loadUnitsFromDir(sourcePath string) ([]*parser.UnitFile, error) {
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
name := file.Name()
|
name := file.Name()
|
||||||
if _, ok := seen[name]; !ok && isExtSupported(name) {
|
if _, ok := seen[name]; !ok && quadlet.IsExtSupported(name) {
|
||||||
path := path.Join(sourcePath, name)
|
path := path.Join(sourcePath, name)
|
||||||
|
|
||||||
Debugf("Loading source unit file %s", path)
|
Debugf("Loading source unit file %s", path)
|
||||||
|
@ -580,33 +364,30 @@ func generateUnitsInfoMap(units []*parser.UnitFile) map[string]*quadlet.UnitInfo
|
||||||
var serviceName string
|
var serviceName string
|
||||||
var containers []string
|
var containers []string
|
||||||
var resourceName string
|
var resourceName string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
serviceName, err = quadlet.GetUnitServiceName(unit)
|
||||||
|
if err != nil {
|
||||||
|
Logf("Error obtaining service name: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(unit.Filename, ".container"):
|
case strings.HasSuffix(unit.Filename, ".container"):
|
||||||
serviceName = quadlet.GetContainerServiceName(unit)
|
|
||||||
// Prefill resouceNames for .container files. This solves network reusing.
|
// Prefill resouceNames for .container files. This solves network reusing.
|
||||||
resourceName = quadlet.GetContainerResourceName(unit)
|
resourceName = quadlet.GetContainerResourceName(unit)
|
||||||
case strings.HasSuffix(unit.Filename, ".volume"):
|
|
||||||
serviceName = quadlet.GetVolumeServiceName(unit)
|
|
||||||
case strings.HasSuffix(unit.Filename, ".kube"):
|
|
||||||
serviceName = quadlet.GetKubeServiceName(unit)
|
|
||||||
case strings.HasSuffix(unit.Filename, ".network"):
|
|
||||||
serviceName = quadlet.GetNetworkServiceName(unit)
|
|
||||||
case strings.HasSuffix(unit.Filename, ".image"):
|
|
||||||
serviceName = quadlet.GetImageServiceName(unit)
|
|
||||||
case strings.HasSuffix(unit.Filename, ".build"):
|
case strings.HasSuffix(unit.Filename, ".build"):
|
||||||
serviceName = quadlet.GetBuildServiceName(unit)
|
|
||||||
// Prefill resouceNames for .build files. This is significantly less complex than
|
// Prefill resouceNames for .build files. This is significantly less complex than
|
||||||
// pre-computing all resourceNames for all Quadlet types (which is rather complex for a few
|
// pre-computing all resourceNames for all Quadlet types (which is rather complex for a few
|
||||||
// types), but still breaks the dependency cycle between .volume and .build ([Volume] can
|
// types), but still breaks the dependency cycle between .volume and .build ([Volume] can
|
||||||
// have Image=some.build, and [Build] can have Volume=some.volume:/some-volume)
|
// have Image=some.build, and [Build] can have Volume=some.volume:/some-volume)
|
||||||
resourceName = quadlet.GetBuiltImageName(unit)
|
resourceName = quadlet.GetBuiltImageName(unit)
|
||||||
case strings.HasSuffix(unit.Filename, ".pod"):
|
case strings.HasSuffix(unit.Filename, ".pod"):
|
||||||
serviceName = quadlet.GetPodServiceName(unit)
|
|
||||||
containers = make([]string, 0)
|
containers = make([]string, 0)
|
||||||
// Prefill resouceNames for .pod files.
|
// Prefill resouceNames for .pod files.
|
||||||
// This is requires for referencing the pod from .container files
|
// This is requires for referencing the pod from .container files
|
||||||
resourceName = quadlet.GetPodResourceName(unit)
|
resourceName = quadlet.GetPodResourceName(unit)
|
||||||
|
case strings.HasSuffix(unit.Filename, ".volume"), strings.HasSuffix(unit.Filename, ".kube"), strings.HasSuffix(unit.Filename, ".network"), strings.HasSuffix(unit.Filename, ".image"):
|
||||||
|
// Do nothing for these case.
|
||||||
default:
|
default:
|
||||||
Logf("Unsupported file type %q", unit.Filename)
|
Logf("Unsupported file type %q", unit.Filename)
|
||||||
continue
|
continue
|
||||||
|
@ -669,7 +450,7 @@ func process() bool {
|
||||||
Debugf("Starting quadlet-generator, output to: %s", outputPath)
|
Debugf("Starting quadlet-generator, output to: %s", outputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
sourcePathsMap := getUnitDirs(isUserFlag)
|
sourcePathsMap := quadlet.GetUnitDirs(isUserFlag)
|
||||||
|
|
||||||
var units []*parser.UnitFile
|
var units []*parser.UnitFile
|
||||||
for _, d := range sourcePathsMap {
|
for _, d := range sourcePathsMap {
|
||||||
|
|
|
@ -3,18 +3,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containers/podman/v5/pkg/systemd/quadlet"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,208 +42,3 @@ func TestIsUnambiguousName(t *testing.T) {
|
||||||
assert.Equal(t, res, test.res, "%q", test.input)
|
assert.Equal(t, res, test.res, "%q", test.input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnitDirs(t *testing.T) {
|
|
||||||
u, err := user.Current()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
uidInt, err := strconv.Atoi(u.Uid)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
if os.Getenv("_UNSHARED") != "true" {
|
|
||||||
unitDirs := getUnitDirs(false)
|
|
||||||
|
|
||||||
resolvedUnitDirAdminUser := resolveUnitDirAdminUser()
|
|
||||||
userLevelFilter := getUserLevelFilter(resolvedUnitDirAdminUser)
|
|
||||||
rootfulPaths := newSearchPaths()
|
|
||||||
appendSubPaths(rootfulPaths, quadlet.UnitDirTemp, false, userLevelFilter)
|
|
||||||
appendSubPaths(rootfulPaths, quadlet.UnitDirAdmin, false, userLevelFilter)
|
|
||||||
appendSubPaths(rootfulPaths, quadlet.UnitDirDistro, false, userLevelFilter)
|
|
||||||
assert.Equal(t, rootfulPaths.sorted, unitDirs, "rootful unit dirs should match")
|
|
||||||
|
|
||||||
configDir, err := os.UserConfigDir()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
rootlessPaths := newSearchPaths()
|
|
||||||
|
|
||||||
systemUserDirLevel := len(strings.Split(resolvedUnitDirAdminUser, string(os.PathSeparator)))
|
|
||||||
nonNumericFilter := getNonNumericFilter(resolvedUnitDirAdminUser, systemUserDirLevel)
|
|
||||||
|
|
||||||
runtimeDir, found := os.LookupEnv("XDG_RUNTIME_DIR")
|
|
||||||
if found {
|
|
||||||
appendSubPaths(rootlessPaths, path.Join(runtimeDir, "containers/systemd"), false, nil)
|
|
||||||
}
|
|
||||||
appendSubPaths(rootlessPaths, path.Join(configDir, "containers/systemd"), false, nil)
|
|
||||||
appendSubPaths(rootlessPaths, filepath.Join(quadlet.UnitDirAdmin, "users"), true, nonNumericFilter)
|
|
||||||
appendSubPaths(rootlessPaths, filepath.Join(quadlet.UnitDirAdmin, "users", u.Uid), true, userLevelFilter)
|
|
||||||
|
|
||||||
unitDirs = getUnitDirs(true)
|
|
||||||
assert.Equal(t, rootlessPaths.sorted, unitDirs, "rootless unit dirs should match")
|
|
||||||
|
|
||||||
// Test that relative path returns an empty list
|
|
||||||
t.Setenv("QUADLET_UNIT_DIRS", "./relative/path")
|
|
||||||
unitDirs = getUnitDirs(false)
|
|
||||||
assert.Equal(t, []string{}, unitDirs)
|
|
||||||
|
|
||||||
name := t.TempDir()
|
|
||||||
t.Setenv("QUADLET_UNIT_DIRS", name)
|
|
||||||
unitDirs = getUnitDirs(false)
|
|
||||||
assert.Equal(t, []string{name}, unitDirs, "rootful should use environment variable")
|
|
||||||
|
|
||||||
unitDirs = getUnitDirs(true)
|
|
||||||
assert.Equal(t, []string{name}, unitDirs, "rootless should use environment variable")
|
|
||||||
|
|
||||||
symLinkTestBaseDir := t.TempDir()
|
|
||||||
|
|
||||||
actualDir := filepath.Join(symLinkTestBaseDir, "actual")
|
|
||||||
err = os.Mkdir(actualDir, 0755)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
innerDir := filepath.Join(actualDir, "inner")
|
|
||||||
err = os.Mkdir(innerDir, 0755)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
symlink := filepath.Join(symLinkTestBaseDir, "symlink")
|
|
||||||
err = os.Symlink(actualDir, symlink)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
t.Setenv("QUADLET_UNIT_DIRS", symlink)
|
|
||||||
unitDirs = getUnitDirs(true)
|
|
||||||
assert.Equal(t, []string{actualDir, innerDir}, unitDirs, "directory resolution should follow symlink")
|
|
||||||
|
|
||||||
// Make a more elborate test with the following structure:
|
|
||||||
// <BASE>/linkToDir - real directory to link to
|
|
||||||
// <BASE>/linkToDir/a - real directory
|
|
||||||
// <BASE>/linkToDir/b - link to <BASE>/unitDir/b/a should be ignored
|
|
||||||
// <BASE>/linkToDir/c - link to <BASE>/unitDir should be ignored
|
|
||||||
// <BASE>/unitDir - start from here
|
|
||||||
// <BASE>/unitDir/a - real directory
|
|
||||||
// <BASE>/unitDir/a/a - real directory
|
|
||||||
// <BASE>/unitDir/a/a/a - real directory
|
|
||||||
// <BASE>/unitDir/b/a - real directory
|
|
||||||
// <BASE>/unitDir/b/b - link to <BASE>/unitDir/a/a should be ignored
|
|
||||||
// <BASE>/unitDir/c - link to <BASE>/linkToDir
|
|
||||||
createDir := func(path, name string, dirs []string) (string, []string) {
|
|
||||||
dirName := filepath.Join(path, name)
|
|
||||||
assert.NotContains(t, dirs, dirName)
|
|
||||||
err = os.Mkdir(dirName, 0755)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
dirs = append(dirs, dirName)
|
|
||||||
return dirName, dirs
|
|
||||||
}
|
|
||||||
|
|
||||||
linkDir := func(path, name, target string) {
|
|
||||||
linkName := filepath.Join(path, name)
|
|
||||||
err = os.Symlink(target, linkName)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
symLinkRecursiveTestBaseDir := t.TempDir()
|
|
||||||
|
|
||||||
expectedDirs := make([]string, 0)
|
|
||||||
// Create <BASE>/unitDir
|
|
||||||
unitsDirPath, expectedDirs := createDir(symLinkRecursiveTestBaseDir, "unitsDir", expectedDirs)
|
|
||||||
// Create <BASE>/unitDir/a
|
|
||||||
aDirPath, expectedDirs := createDir(unitsDirPath, "a", expectedDirs)
|
|
||||||
// Create <BASE>/unitDir/a/a
|
|
||||||
aaDirPath, expectedDirs := createDir(aDirPath, "a", expectedDirs)
|
|
||||||
// Create <BASE>/unitDir/a/a/a
|
|
||||||
_, expectedDirs = createDir(aaDirPath, "a", expectedDirs)
|
|
||||||
// Create <BASE>/unitDir/a/b
|
|
||||||
_, expectedDirs = createDir(aDirPath, "b", expectedDirs)
|
|
||||||
// Create <BASE>/unitDir/b
|
|
||||||
bDirPath, expectedDirs := createDir(unitsDirPath, "b", expectedDirs)
|
|
||||||
// Create <BASE>/unitDir/b/a
|
|
||||||
baDirPath, expectedDirs := createDir(bDirPath, "a", expectedDirs)
|
|
||||||
// Create <BASE>/linkToDir
|
|
||||||
linkToDirPath, expectedDirs := createDir(symLinkRecursiveTestBaseDir, "linkToDir", expectedDirs)
|
|
||||||
// Create <BASE>/linkToDir/a
|
|
||||||
_, expectedDirs = createDir(linkToDirPath, "a", expectedDirs)
|
|
||||||
|
|
||||||
// Link <BASE>/unitDir/b/b to <BASE>/unitDir/a/a
|
|
||||||
linkDir(bDirPath, "b", aaDirPath)
|
|
||||||
// Link <BASE>/linkToDir/b to <BASE>/unitDir/b/a
|
|
||||||
linkDir(linkToDirPath, "b", baDirPath)
|
|
||||||
// Link <BASE>/linkToDir/c to <BASE>/unitDir
|
|
||||||
linkDir(linkToDirPath, "c", unitsDirPath)
|
|
||||||
// Link <BASE>/unitDir/c to <BASE>/linkToDir
|
|
||||||
linkDir(unitsDirPath, "c", linkToDirPath)
|
|
||||||
|
|
||||||
t.Setenv("QUADLET_UNIT_DIRS", unitsDirPath)
|
|
||||||
unitDirs = getUnitDirs(true)
|
|
||||||
assert.Equal(t, expectedDirs, unitDirs, "directory resolution should follow symlink")
|
|
||||||
// remove the temporary directory at the end of the program
|
|
||||||
defer os.RemoveAll(symLinkTestBaseDir)
|
|
||||||
|
|
||||||
// because chroot is only available for root,
|
|
||||||
// unshare the namespace and map user to root
|
|
||||||
c := exec.Command("/proc/self/exe", os.Args[1:]...)
|
|
||||||
c.Stdin = os.Stdin
|
|
||||||
c.Stdout = os.Stdout
|
|
||||||
c.Stderr = os.Stderr
|
|
||||||
c.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Cloneflags: syscall.CLONE_NEWUSER,
|
|
||||||
UidMappings: []syscall.SysProcIDMap{
|
|
||||||
{
|
|
||||||
ContainerID: 0,
|
|
||||||
HostID: uidInt,
|
|
||||||
Size: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
c.Env = append(os.Environ(), "_UNSHARED=true")
|
|
||||||
err = c.Run()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
} else {
|
|
||||||
fmt.Println(os.Args)
|
|
||||||
|
|
||||||
symLinkTestBaseDir := t.TempDir()
|
|
||||||
rootF, err := os.Open("/")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer rootF.Close()
|
|
||||||
defer func() {
|
|
||||||
err := rootF.Chdir()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = syscall.Chroot(".")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}()
|
|
||||||
err = syscall.Chroot(symLinkTestBaseDir)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = os.MkdirAll(quadlet.UnitDirAdmin, 0755)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
err = os.RemoveAll(quadlet.UnitDirAdmin)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
createDir := func(path, name string) string {
|
|
||||||
dirName := filepath.Join(path, name)
|
|
||||||
err = os.Mkdir(dirName, 0755)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
return dirName
|
|
||||||
}
|
|
||||||
|
|
||||||
linkDir := func(path, name, target string) {
|
|
||||||
linkName := filepath.Join(path, name)
|
|
||||||
err = os.Symlink(target, linkName)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
systemdDir := createDir("/", "systemd")
|
|
||||||
userDir := createDir("/", "users")
|
|
||||||
linkDir(systemdDir, "users", userDir)
|
|
||||||
linkDir(quadlet.UnitDirAdmin, "", systemdDir)
|
|
||||||
|
|
||||||
uidDir := createDir(userDir, u.Uid)
|
|
||||||
uidDir2 := createDir(userDir, strconv.Itoa(uidInt+1))
|
|
||||||
userInternalDir := createDir(userDir, "internal")
|
|
||||||
|
|
||||||
// Make sure QUADLET_UNIT_DIRS is not set
|
|
||||||
t.Setenv("QUADLET_UNIT_DIRS", "")
|
|
||||||
// Test Rootful
|
|
||||||
unitDirs := getUnitDirs(false)
|
|
||||||
assert.NotContains(t, unitDirs, userDir, "rootful should not contain rootless")
|
|
||||||
assert.NotContains(t, unitDirs, userInternalDir, "rootful should not contain rootless")
|
|
||||||
|
|
||||||
// Test Rootless
|
|
||||||
unitDirs = getUnitDirs(true)
|
|
||||||
assert.NotContains(t, unitDirs, uidDir2, "rootless should not contain other users'")
|
|
||||||
assert.Contains(t, unitDirs, userInternalDir, "rootless should contain sub-directories of users dir")
|
|
||||||
assert.Contains(t, unitDirs, uidDir, "rootless should contain the directory for its UID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -81,6 +81,8 @@ Commands
|
||||||
|
|
||||||
:doc:`push <markdown/podman-push.1>` Push an image to a specified destination
|
:doc:`push <markdown/podman-push.1>` Push an image to a specified destination
|
||||||
|
|
||||||
|
:doc:`quadlet <markdown/podman-quadlet.1>` Allows users to manage Quadlets
|
||||||
|
|
||||||
:doc:`rename <markdown/podman-rename.1>` Rename an existing container
|
:doc:`rename <markdown/podman-rename.1>` Rename an existing container
|
||||||
|
|
||||||
:doc:`restart <markdown/podman-restart.1>` Restart one or more containers
|
:doc:`restart <markdown/podman-restart.1>` Restart one or more containers
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
% podman-quadlet-install 1
|
||||||
|
|
||||||
|
## NAME
|
||||||
|
podman\-quadlet\-install - Install one or more quadlets
|
||||||
|
|
||||||
|
## SYNOPSIS
|
||||||
|
**podman quadlet install** [*options*] *path-or-url* [*path-or-url*]...
|
||||||
|
|
||||||
|
## DESCRIPTION
|
||||||
|
|
||||||
|
Install one or more Quadlets for the current user. Quadlets may be specified as local files, Web URLs.
|
||||||
|
|
||||||
|
## OPTIONS
|
||||||
|
|
||||||
|
#### **--reload-systemd**
|
||||||
|
|
||||||
|
Reload systemd after installing Quadlets (default true).
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
Install quadlet from a file.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ podman quadlet install /path/to/myquadlet.container
|
||||||
|
```
|
||||||
|
Install quadlet from a dir.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ podman quadlet install /path/to/dir/
|
||||||
|
```
|
||||||
|
|
||||||
|
Install quadlet from a url
|
||||||
|
```
|
||||||
|
$ podman quadlet install https://github.com/containers/podman/blob/main/test/e2e/quadlet/basic.container
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEE ALSO
|
||||||
|
**[podman(1)](podman.1.md)**, **[podman-quadlet(1)](podman-quadlet.1.md)**
|
|
@ -0,0 +1,54 @@
|
||||||
|
% podman-quadlet-list 1
|
||||||
|
|
||||||
|
## NAME
|
||||||
|
podman\-quadlet\-list - List installed quadlets
|
||||||
|
|
||||||
|
## SYNOPSIS
|
||||||
|
**podman quadlet list** [*options*]
|
||||||
|
|
||||||
|
## DESCRIPTION
|
||||||
|
|
||||||
|
List all Quadlets configured for the current user.
|
||||||
|
|
||||||
|
## OPTIONS
|
||||||
|
|
||||||
|
#### **--filter**, **-f**
|
||||||
|
|
||||||
|
Filter output based on conditions give.
|
||||||
|
|
||||||
|
The *filters* argument format is of `key=value`. If there is more than one *filter*, then pass multiple OPTIONS: **--filter** *foo=bar* **--filter** *bif=baz*.
|
||||||
|
|
||||||
|
Supported filters:
|
||||||
|
|
||||||
|
| Filter | Description |
|
||||||
|
|------------|--------------------------------------------------------------------------------------------------|
|
||||||
|
| name | Filter by quadlet name. |
|
||||||
|
|
||||||
|
#### **--format**
|
||||||
|
|
||||||
|
Pretty-print output to JSON or using a Go template (default "{{range .}}{{.Name}}\t{{.UnitName}}\t{{.Path}}\t{{.Status}}\n{{end -}}")
|
||||||
|
|
||||||
|
Print results with a Go template.
|
||||||
|
|
||||||
|
| **Placeholder** | **Description** |
|
||||||
|
|-----------------|------------------------------------------------|
|
||||||
|
| .Name | Name of the Quadlet file |
|
||||||
|
| .Path | Quadlet file path on disk |
|
||||||
|
| .Status | Quadlet status corresponding to systemd unit |
|
||||||
|
| .UnitName | Systemd unit name corresponding to quadlet |
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
Filter list by name.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ podman quadlet list --filter 'name=test*'
|
||||||
|
```
|
||||||
|
|
||||||
|
Format list output for a specific field.
|
||||||
|
```
|
||||||
|
$ podman quadlet list --format '{{ .Unit }}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEE ALSO
|
||||||
|
**[podman(1)](podman.1.md)**, **[podman-quadlet(1)](podman-quadlet.1.md)**
|
|
@ -0,0 +1,20 @@
|
||||||
|
% podman-quadlet-print 1
|
||||||
|
|
||||||
|
## NAME
|
||||||
|
podman\-quadlet\-print - Display the contents of a quadlet
|
||||||
|
|
||||||
|
## SYNOPSIS
|
||||||
|
**podman quadlet print** *quadlet-name*
|
||||||
|
|
||||||
|
## DESCRIPTION
|
||||||
|
|
||||||
|
Print the contents of a Quadlet, displaying the file including all comments.
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
```
|
||||||
|
$ podman quadlet print myquadlet.container
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEE ALSO
|
||||||
|
**[podman(1)](podman.1.md)**, **[podman-quadlet(1)](podman-quadlet.1.md)**
|
|
@ -0,0 +1,38 @@
|
||||||
|
% podman-quadlet-rm 1
|
||||||
|
|
||||||
|
## NAME
|
||||||
|
podman\-quadlet\-rm - Removes an installed quadlet
|
||||||
|
|
||||||
|
## SYNOPSIS
|
||||||
|
**podman quadlet rm** [*options*] *quadlet-name* [*quadlet-name*]...
|
||||||
|
|
||||||
|
## DESCRIPTION
|
||||||
|
|
||||||
|
Remove one or more installed Quadlets from the current user.
|
||||||
|
|
||||||
|
## OPTIONS
|
||||||
|
|
||||||
|
#### **--all**, **-a**
|
||||||
|
|
||||||
|
Remove all Quadlets for the current user.
|
||||||
|
|
||||||
|
#### **--force**, **-f**
|
||||||
|
|
||||||
|
Remove running quadlets.
|
||||||
|
|
||||||
|
#### **--ignore**, **-i**
|
||||||
|
|
||||||
|
Do not error for Quadlets that do not exist.
|
||||||
|
|
||||||
|
#### **--reload-systemd**
|
||||||
|
|
||||||
|
Reload systemd after removal
|
||||||
|
|
||||||
|
## EXAMPLES
|
||||||
|
|
||||||
|
```
|
||||||
|
$ podman quadlet rm myquadlet.container
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEE ALSO
|
||||||
|
**[podman(1)](podman.1.md)**, **[podman-quadlet(1)](podman-quadlet.1.md)**
|
|
@ -0,0 +1,25 @@
|
||||||
|
% podman-quadlet 1
|
||||||
|
|
||||||
|
## NAME
|
||||||
|
podman\-quadlet - Allows users to manage Quadlets
|
||||||
|
|
||||||
|
## SYNOPSIS
|
||||||
|
**podman quadlet** *subcommand*
|
||||||
|
|
||||||
|
## DESCRIPTION
|
||||||
|
`podman quadlet` is a set of subcommands that manage Quadlets.
|
||||||
|
|
||||||
|
Podman Quadlets allow users to manage containers, pods, volumes, networks, and images declaratively via systemd unit files, streamlining container management on Linux systems without the complexity of full orchestration tools like Kubernetes
|
||||||
|
|
||||||
|
|
||||||
|
## SUBCOMMANDS
|
||||||
|
|
||||||
|
| Command | Man Page | Description |
|
||||||
|
|---------|------------------------------------------------------------|--------------------------------------------------------------|
|
||||||
|
| install | [podman-quadlet-install(1)](podman-quadlet-install.1.md) | Install one or more quadlets |
|
||||||
|
| list | [podman-quadlet-list(1)](podman-quadlet-list.1.md) | List installed quadlets |
|
||||||
|
| print | [podman-quadlet-print(1)](podman-quadlet-print.1.md) | Display the contents of a quadlet |
|
||||||
|
| rm | [podman-quadlet-rm(1)](podman-quadlet-rm.1.md) | Removes an installed quadlet |
|
||||||
|
|
||||||
|
## SEE ALSO
|
||||||
|
**[podman(1)](podman.1.md)**
|
|
@ -371,6 +371,7 @@ the exit codes follow the `chroot` standard, see below:
|
||||||
| [podman-ps(1)](podman-ps.1.md) | Print out information about containers. |
|
| [podman-ps(1)](podman-ps.1.md) | Print out information about containers. |
|
||||||
| [podman-pull(1)](podman-pull.1.md) | Pull an image from a registry. |
|
| [podman-pull(1)](podman-pull.1.md) | Pull an image from a registry. |
|
||||||
| [podman-push(1)](podman-push.1.md) | Push an image, manifest list or image index from local storage to elsewhere. |
|
| [podman-push(1)](podman-push.1.md) | Push an image, manifest list or image index from local storage to elsewhere. |
|
||||||
|
| [podman-quadlet(1)](podman-quadlet.1.md) | Allows users to manage Quadlets. |
|
||||||
| [podman-rename(1)](podman-rename.1.md) | Rename an existing container. |
|
| [podman-rename(1)](podman-rename.1.md) | Rename an existing container. |
|
||||||
| [podman-restart(1)](podman-restart.1.md) | Restart one or more containers. |
|
| [podman-restart(1)](podman-restart.1.md) | Restart one or more containers. |
|
||||||
| [podman-rm(1)](podman-rm.1.md) | Remove one or more containers. |
|
| [podman-rm(1)](podman-rm.1.md) | Remove one or more containers. |
|
||||||
|
|
|
@ -93,6 +93,10 @@ type ContainerEngine interface { //nolint:interfacebloat
|
||||||
PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error)
|
PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error)
|
||||||
PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error)
|
PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error)
|
||||||
PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error)
|
PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error)
|
||||||
|
QuadletInstall(ctx context.Context, pathsOrURLs []string, options QuadletInstallOptions) (*QuadletInstallReport, error)
|
||||||
|
QuadletList(ctx context.Context, options QuadletListOptions) ([]*ListQuadlet, error)
|
||||||
|
QuadletPrint(ctx context.Context, quadlet string) (string, error)
|
||||||
|
QuadletRemove(ctx context.Context, quadlets []string, options QuadletRemoveOptions) (*QuadletRemoveReport, error)
|
||||||
Renumber(ctx context.Context) error
|
Renumber(ctx context.Context) error
|
||||||
Reset(ctx context.Context) error
|
Reset(ctx context.Context) error
|
||||||
SetupRootless(ctx context.Context, noMoveProcess bool, cgroupMode string) error
|
SetupRootless(ctx context.Context, noMoveProcess bool, cgroupMode string) error
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package entities
|
||||||
|
|
||||||
|
// QuadletInstallOptions contains options to the `podman quadlet install` command
|
||||||
|
type QuadletInstallOptions struct {
|
||||||
|
// Whether to reload systemd after installation is completed
|
||||||
|
ReloadSystemd bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadletInstallReport contains the output of the `quadlet install` command
|
||||||
|
// including what files were successfully installed (and to where), and what
|
||||||
|
// files errored (and why).
|
||||||
|
type QuadletInstallReport struct {
|
||||||
|
// InstalledQuadlets is a map of the path of the quadlet file to be installed
|
||||||
|
// to where it was installed to.
|
||||||
|
InstalledQuadlets map[string]string
|
||||||
|
// QuadletErrors is a map of the path of the quadlet file to be installed
|
||||||
|
// to the error that occurred attempting to install it
|
||||||
|
QuadletErrors map[string]error
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadletListOptions contains options to the `podman quadlet list` command.
|
||||||
|
type QuadletListOptions struct {
|
||||||
|
// Filters contains filters that will limit what Quadlets are displayed
|
||||||
|
Filters []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// A ListQuadlet is a single Quadlet to be listed by `podman quadlet list`
|
||||||
|
type ListQuadlet struct {
|
||||||
|
// Name is the name of the Quadlet file
|
||||||
|
Name string
|
||||||
|
// UnitName is the name of the systemd unit created from the Quadlet.
|
||||||
|
// May be empty if systemd has not be reloaded since it was installed.
|
||||||
|
UnitName string
|
||||||
|
// Path to the Quadlet on disk
|
||||||
|
Path string
|
||||||
|
// What is the status of the Quadlet - if present in systemd, will be a
|
||||||
|
// systemd status, else will mention if the Quadlet has syntax errors
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadletRemoveOptions contains parameters for removing Quadlets
|
||||||
|
type QuadletRemoveOptions struct {
|
||||||
|
// Force indicates that running quadlets should be removed as well
|
||||||
|
Force bool
|
||||||
|
// All indicates all quadlets should be removed
|
||||||
|
All bool
|
||||||
|
// Ignore indicates that missing quadlets should not cause an error
|
||||||
|
Ignore bool
|
||||||
|
// ReloadSystemd determines whether systemd will be reloaded after the Quadlet is removed.
|
||||||
|
ReloadSystemd bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuadletRemoveReport contains the results of an operation to remove obe or more quadlets
|
||||||
|
type QuadletRemoveReport struct {
|
||||||
|
// Removed is a list of quadlets that were successfully removed
|
||||||
|
Removed []string
|
||||||
|
// Errors is a map of Quadlet name to error that occurred during removal.
|
||||||
|
Errors map[string]error
|
||||||
|
}
|
|
@ -0,0 +1,584 @@
|
||||||
|
//go:build !remote
|
||||||
|
// +build !remote
|
||||||
|
|
||||||
|
package abi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||||
|
"github.com/containers/podman/v5/pkg/rootless"
|
||||||
|
"github.com/containers/podman/v5/pkg/systemd"
|
||||||
|
"github.com/containers/podman/v5/pkg/systemd/parser"
|
||||||
|
systemdquadlet "github.com/containers/podman/v5/pkg/systemd/quadlet"
|
||||||
|
"github.com/containers/podman/v5/pkg/util"
|
||||||
|
"github.com/coreos/go-systemd/v22/dbus"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Install one or more Quadlet files
|
||||||
|
func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []string, options entities.QuadletInstallOptions) (*entities.QuadletInstallReport, error) {
|
||||||
|
// Is systemd available to the current user?
|
||||||
|
// We cannot proceed if not.
|
||||||
|
conn, err := systemd.ConnectToDBUS()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connecting to systemd dbus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is Quadlet installed? No point if not.
|
||||||
|
quadletPath := "/usr/lib/systemd/system-generators/podman-system-generator"
|
||||||
|
quadletStat, err := os.Stat(quadletPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot stat Quadlet generator, Quadlet may not be installed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quadletStat.Mode().IsRegular() || quadletStat.Mode()&0100 == 0 {
|
||||||
|
return nil, fmt.Errorf("no valid Quadlet binary installed to %q, unable to use Quadlet", quadletPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
installDir := systemdquadlet.UnitDirAdmin
|
||||||
|
if rootless.IsRootless() {
|
||||||
|
// Install just for the user in question.
|
||||||
|
quadletRootlessDirs := systemdquadlet.GetUnitDirs(true)
|
||||||
|
|
||||||
|
for _, dir := range quadletRootlessDirs {
|
||||||
|
// Prefer /etc/containers/systemd/users(/$UID)
|
||||||
|
if strings.HasPrefix(dir, systemdquadlet.UnitDirAdmin) {
|
||||||
|
// Does it exist and can we write to it? If it doesn't, we cannot use it.
|
||||||
|
stat, err := os.Stat(dir)
|
||||||
|
if err != nil || !stat.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if unix.Access(dir, unix.W_OK) == nil {
|
||||||
|
installDir = dir
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// If we don't have write permission let's find another path.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we can't use the /etc/ directory, use what is available.
|
||||||
|
// The permanent directory should always be after the temporary one
|
||||||
|
// if both exist, so iterate through all directories.
|
||||||
|
installDir = dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Going to install Quadlet to directory %s", installDir)
|
||||||
|
|
||||||
|
stat, err := os.Stat(installDir)
|
||||||
|
if rootless.IsRootless() {
|
||||||
|
// Make the directory if it doesn't exist
|
||||||
|
if err != nil && errors.Is(err, fs.ErrNotExist) {
|
||||||
|
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to create Quadlet install path %s: %w", installDir, err)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to stat Quadlet install path %s: %w", installDir, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Package manager should have created the dir for root Podman.
|
||||||
|
// So just check that it exists.
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to stat Quadlet install path %s: %w", installDir, err)
|
||||||
|
}
|
||||||
|
if !stat.IsDir() {
|
||||||
|
return nil, fmt.Errorf("install path %s for Quadlets is not a directory", installDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installReport := entities.QuadletInstallReport{
|
||||||
|
InstalledQuadlets: make(map[string]string),
|
||||||
|
QuadletErrors: make(map[string]error),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over all given URLs
|
||||||
|
for _, toInstall := range pathsOrURLs {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(toInstall, "http://") || strings.HasPrefix(toInstall, "https://"):
|
||||||
|
r, err := http.Get(toInstall)
|
||||||
|
if err != nil {
|
||||||
|
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to download URL %s: %w", toInstall, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
quadletExtension := getFileExtension(r)
|
||||||
|
if quadletExtension == "" {
|
||||||
|
quadletExtension = ".container"
|
||||||
|
}
|
||||||
|
// It's a URL. Pull to temporary file.
|
||||||
|
tmpFile, err := os.CreateTemp("", "quadlet-dl-*"+quadletExtension)
|
||||||
|
if err != nil {
|
||||||
|
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to create temporary file to download URL %s: %w", toInstall, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
tmpFile.Close()
|
||||||
|
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||||
|
logrus.Errorf("Unable to remove temporary file %q: %v", tmpFile.Name(), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer r.Body.Close()
|
||||||
|
_, err = io.Copy(tmpFile, r.Body)
|
||||||
|
if err != nil {
|
||||||
|
installReport.QuadletErrors[toInstall] = fmt.Errorf("populating temporary file: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
installedPath, err := ic.installQuadlet(ctx, tmpFile.Name(), "", installDir)
|
||||||
|
if err != nil {
|
||||||
|
installReport.QuadletErrors[toInstall] = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
installReport.InstalledQuadlets[toInstall] = installedPath
|
||||||
|
default:
|
||||||
|
fileInfo, err := os.Stat(toInstall)
|
||||||
|
if err != nil {
|
||||||
|
installReport.QuadletErrors[toInstall] = err
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
// If toInstall is a directory, iterate over its files
|
||||||
|
files, err := os.ReadDir(toInstall)
|
||||||
|
if err != nil {
|
||||||
|
installReport.QuadletErrors[toInstall] = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(toInstall, file.Name())
|
||||||
|
if !systemdquadlet.IsExtSupported(filePath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
installedPath, err := ic.installQuadlet(ctx, filePath, "", installDir)
|
||||||
|
if err != nil {
|
||||||
|
installReport.QuadletErrors[filePath] = err
|
||||||
|
} else {
|
||||||
|
installReport.InstalledQuadlets[filePath] = installedPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If toInstall is a single file, execute the original logic
|
||||||
|
installedPath, err := ic.installQuadlet(ctx, toInstall, "", installDir)
|
||||||
|
if err != nil {
|
||||||
|
installReport.QuadletErrors[toInstall] = err
|
||||||
|
} else {
|
||||||
|
installReport.InstalledQuadlets[toInstall] = installedPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform a Quadlet dry-run to validate the syntax of our quadlets
|
||||||
|
// TODO: This can definitely be improved.
|
||||||
|
// We can't easily validate single quadlets (you can depend on other quadlets, so validation
|
||||||
|
// really has to parse everything), but we should be able to output in a machine-readable format
|
||||||
|
// (JSON, maybe?) so we can easily associated error to quadlet here, and return better
|
||||||
|
// results.
|
||||||
|
var validateErr error
|
||||||
|
quadletArgs := []string{"--dryrun"}
|
||||||
|
if rootless.IsRootless() {
|
||||||
|
quadletArgs = append(quadletArgs, "--user")
|
||||||
|
}
|
||||||
|
quadletCmd := exec.Command(quadletPath, quadletArgs...)
|
||||||
|
out, err := quadletCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Error validating Quadlet syntax")
|
||||||
|
fmt.Fprint(os.Stderr, string(out))
|
||||||
|
validateErr = errors.New("validating Quadlet syntax failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Should we still do this if the above validation errored?
|
||||||
|
if options.ReloadSystemd {
|
||||||
|
if err := conn.ReloadContext(ctx); err != nil {
|
||||||
|
return &installReport, fmt.Errorf("reloading systemd: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &installReport, validateErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracts file extension from Content-Disposition or URL
|
||||||
|
func getFileExtension(resp *http.Response) string {
|
||||||
|
// Try Content-Disposition header first
|
||||||
|
cd := resp.Header.Get("Content-Disposition")
|
||||||
|
if cd != "" {
|
||||||
|
re := regexp.MustCompile(`(?i)filename="?([^"]+)"?`)
|
||||||
|
matches := re.FindStringSubmatch(cd)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return path.Ext(matches[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: use URL path
|
||||||
|
ext := path.Ext(resp.Request.URL.Path)
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install a single Quadlet from a path on local disk to the given install directory.
|
||||||
|
// Perform some minimal validation, but not much.
|
||||||
|
// We can't know about a lot of problems without running the Quadlet binary, which we
|
||||||
|
// only want to do once.
|
||||||
|
func (ic *ContainerEngine) installQuadlet(_ context.Context, path, destName, installDir string) (string, error) {
|
||||||
|
// First, validate that the source path exists and is a file
|
||||||
|
stat, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("quadlet to install %q does not exist or cannot be read: %w", path, err)
|
||||||
|
}
|
||||||
|
if stat.IsDir() {
|
||||||
|
return "", fmt.Errorf("quadlet to install %q is not a file", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalPath := filepath.Join(installDir, filepath.Base(filepath.Clean(path)))
|
||||||
|
if destName != "" {
|
||||||
|
finalPath = filepath.Join(installDir, destName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second, validate that the dest path does NOT exist.
|
||||||
|
// TODO: Overwrite flag?
|
||||||
|
if _, err := os.Stat(finalPath); err == nil {
|
||||||
|
return "", fmt.Errorf("a Quadlet with name %s already exists, refusing to overwrite", filepath.Base(finalPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate extension is valid
|
||||||
|
if !systemdquadlet.IsExtSupported(finalPath) {
|
||||||
|
return "", fmt.Errorf("%q is not a supported Quadlet file type", filepath.Ext(finalPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the file in
|
||||||
|
contents, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading source file %q: %w", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(finalPath, contents, 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("writing Quadlet %q to %q: %w", path, finalPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: It would be nice to do single-file validation here, and remove the file if it fails.
|
||||||
|
|
||||||
|
return finalPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the paths of all quadlets available to the current user
|
||||||
|
func getAllQuadletPaths() []string {
|
||||||
|
var quadletPaths []string
|
||||||
|
quadletDirs := systemdquadlet.GetUnitDirs(rootless.IsRootless())
|
||||||
|
for _, dir := range quadletDirs {
|
||||||
|
dents, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
// This is perfectly normal, some quadlet directories aren't created by the package
|
||||||
|
logrus.Infof("Cannot list Quadlet directory %s: %v", dir, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logrus.Debugf("Checking for quadlets in %q", dir)
|
||||||
|
for _, dent := range dents {
|
||||||
|
if systemdquadlet.IsExtSupported(dent.Name()) && !dent.IsDir() {
|
||||||
|
logrus.Debugf("Found quadlet %q", dent.Name())
|
||||||
|
quadletPaths = append(quadletPaths, filepath.Join(dir, dent.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return quadletPaths
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate systemd service name for a Quadlet from full path to the Quadlet file
|
||||||
|
func getQuadletServiceName(quadletPath string) (string, error) {
|
||||||
|
unit, err := parser.ParseUnitFile(quadletPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parsing Quadlet file %s: %w", quadletPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName, err := systemdquadlet.GetUnitServiceName(unit)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("generating service name for Quadlet %s: %w", filepath.Base(quadletPath), err)
|
||||||
|
}
|
||||||
|
return serviceName + ".service", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuadletFilter func(q *entities.ListQuadlet) bool
|
||||||
|
|
||||||
|
func generateQuadletFilter(filter string, filterValues []string) (func(q *entities.ListQuadlet) bool, error) {
|
||||||
|
switch filter {
|
||||||
|
case "name":
|
||||||
|
return func(q *entities.ListQuadlet) bool {
|
||||||
|
res := util.StringMatchRegexSlice(q.Name, filterValues)
|
||||||
|
return res
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s is not a valid filter", filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.QuadletListOptions) ([]*entities.ListQuadlet, error) {
|
||||||
|
// Is systemd available to the current user?
|
||||||
|
// We cannot proceed if not.
|
||||||
|
conn, err := dbus.NewWithContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connecting to systemd dbus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
quadletPaths := getAllQuadletPaths()
|
||||||
|
|
||||||
|
// Create filter functions
|
||||||
|
filterFuncs := make([]func(q *entities.ListQuadlet) bool, 0, len(options.Filters))
|
||||||
|
filterMap := make(map[string][]string)
|
||||||
|
for _, f := range options.Filters {
|
||||||
|
fname, filter, hasFilter := strings.Cut(f, "=")
|
||||||
|
if !hasFilter {
|
||||||
|
return nil, fmt.Errorf("invalid filter %q", f)
|
||||||
|
}
|
||||||
|
filterMap[fname] = append(filterMap[fname], filter)
|
||||||
|
}
|
||||||
|
for fname, filter := range filterMap {
|
||||||
|
filterFunc, err := generateQuadletFilter(fname, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filterFuncs = append(filterFuncs, filterFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
reports := make([]*entities.ListQuadlet, 0, len(quadletPaths))
|
||||||
|
allServiceNames := make([]string, 0, len(quadletPaths))
|
||||||
|
partialReports := make(map[string]entities.ListQuadlet)
|
||||||
|
|
||||||
|
for _, path := range quadletPaths {
|
||||||
|
report := entities.ListQuadlet{
|
||||||
|
Name: filepath.Base(path),
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName, err := getQuadletServiceName(path)
|
||||||
|
if err != nil {
|
||||||
|
report.Status = err.Error()
|
||||||
|
reports = append(reports, &report)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
partialReports[serviceName] = report
|
||||||
|
allServiceNames = append(allServiceNames, serviceName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get status of all systemd units with given names.
|
||||||
|
statuses, err := conn.ListUnitsByNamesContext(ctx, allServiceNames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("querying systemd for unit status: %w", err)
|
||||||
|
}
|
||||||
|
if len(statuses) != len(allServiceNames) {
|
||||||
|
logrus.Warnf("Queried for %d services but received %d responses", len(allServiceNames), len(statuses))
|
||||||
|
}
|
||||||
|
for _, unitStatus := range statuses {
|
||||||
|
report, ok := partialReports[unitStatus.Name]
|
||||||
|
if !ok {
|
||||||
|
logrus.Errorf("Unexpected unit returned by systemd - was not searching for %s", unitStatus.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Unit %s has status %s %s %s", unitStatus.Name, unitStatus.LoadState, unitStatus.ActiveState, unitStatus.SubState)
|
||||||
|
report.UnitName = unitStatus.Name
|
||||||
|
|
||||||
|
// Unit is not loaded
|
||||||
|
if unitStatus.LoadState != "loaded" {
|
||||||
|
report.Status = "Not loaded"
|
||||||
|
reports = append(reports, &report)
|
||||||
|
delete(partialReports, unitStatus.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
report.Status = fmt.Sprintf("%s/%s", unitStatus.ActiveState, unitStatus.SubState)
|
||||||
|
|
||||||
|
reports = append(reports, &report)
|
||||||
|
delete(partialReports, unitStatus.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should not happen.
|
||||||
|
// Systemd will give us output for everything we sent to them, even if it's not a valid unit.
|
||||||
|
// We can find them with LoadState, as we do above.
|
||||||
|
// Handle it anyways because it's easy enough to do.
|
||||||
|
for _, report := range partialReports {
|
||||||
|
report.Status = "Not loaded"
|
||||||
|
reports = append(reports, &report)
|
||||||
|
}
|
||||||
|
|
||||||
|
finalReports := make([]*entities.ListQuadlet, 0, len(reports))
|
||||||
|
for _, report := range reports {
|
||||||
|
include := true
|
||||||
|
for _, filterFunc := range filterFuncs {
|
||||||
|
include = filterFunc(report)
|
||||||
|
}
|
||||||
|
if include {
|
||||||
|
finalReports = append(finalReports, report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalReports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve path to a Quadlet file given full name including extension
|
||||||
|
func getQuadletByName(name string) (string, error) {
|
||||||
|
// Check if we were given a valid extension
|
||||||
|
if !systemdquadlet.IsExtSupported(name) {
|
||||||
|
return "", fmt.Errorf("%q is not a supported quadlet file type", filepath.Ext(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
quadletDirs := systemdquadlet.GetUnitDirs(rootless.IsRootless())
|
||||||
|
for _, dir := range quadletDirs {
|
||||||
|
testPath := filepath.Join(dir, name)
|
||||||
|
if _, err := os.Stat(testPath); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return "", fmt.Errorf("cannot stat quadlet at path %q: %w", testPath, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return testPath, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("could not locate quadlet %q in any supported quadlet directory", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) QuadletPrint(ctx context.Context, quadlet string) (string, error) {
|
||||||
|
quadletPath, err := getQuadletByName(quadlet)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
contents, err := os.ReadFile(quadletPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading quadlet %q contents: %w", quadletPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(contents), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string, options entities.QuadletRemoveOptions) (*entities.QuadletRemoveReport, error) {
|
||||||
|
report := entities.QuadletRemoveReport{
|
||||||
|
Errors: make(map[string]error),
|
||||||
|
Removed: []string{},
|
||||||
|
}
|
||||||
|
allQuadletPaths := make([]string, 0, len(quadlets))
|
||||||
|
allServiceNames := make([]string, 0, len(quadlets))
|
||||||
|
runningQuadlets := make([]string, 0, len(quadlets))
|
||||||
|
serviceNameToQuadletName := make(map[string]string)
|
||||||
|
needReload := false
|
||||||
|
|
||||||
|
// Early escape: if 0 quadlets are requested, bail immediately without error.
|
||||||
|
if len(quadlets) == 0 && !options.All {
|
||||||
|
return nil, errors.New("must provide at least 1 quadlet to remove")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is systemd available to the current user?
|
||||||
|
// We cannot proceed if not.
|
||||||
|
conn, err := systemd.ConnectToDBUS()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connecting to systemd dbus: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.All {
|
||||||
|
allQuadlets := getAllQuadletPaths()
|
||||||
|
quadlets = allQuadlets
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, quadlet := range quadlets {
|
||||||
|
quadletPath, err := getQuadletByName(quadlet)
|
||||||
|
if options.All {
|
||||||
|
quadletPath = quadlet
|
||||||
|
}
|
||||||
|
if !options.All && err != nil {
|
||||||
|
// All implies Ignore, because the only reason we'd see an error here with all
|
||||||
|
// is if the quadlet was removed in a TOCTOU scenario.
|
||||||
|
if options.Ignore {
|
||||||
|
report.Removed = append(report.Removed, quadlet)
|
||||||
|
} else {
|
||||||
|
report.Errors[quadlet] = err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
allQuadletPaths = append(allQuadletPaths, quadletPath)
|
||||||
|
|
||||||
|
serviceName, err := getQuadletServiceName(quadletPath)
|
||||||
|
if err != nil {
|
||||||
|
report.Errors[quadlet] = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
allServiceNames = append(allServiceNames, serviceName)
|
||||||
|
serviceNameToQuadletName[serviceName] = quadlet
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(allServiceNames) != 0 {
|
||||||
|
// Check if units are loaded into systemd, and further if they are running.
|
||||||
|
// If running and force is not set, error.
|
||||||
|
// If force is set, try and stop the unit.
|
||||||
|
statuses, err := conn.ListUnitsByNamesContext(ctx, allServiceNames)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("querying systemd for unit status: %w", err)
|
||||||
|
}
|
||||||
|
for _, unitStatus := range statuses {
|
||||||
|
quadletName := serviceNameToQuadletName[unitStatus.Name]
|
||||||
|
|
||||||
|
if unitStatus.LoadState != "loaded" {
|
||||||
|
// Nothing to do here if it doesn't exist in systemd
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
needReload = true
|
||||||
|
if unitStatus.ActiveState == "active" {
|
||||||
|
if !options.Force {
|
||||||
|
report.Errors[quadletName] = fmt.Errorf("quadlet %s is running and force is not set, refusing to remove", quadletName)
|
||||||
|
runningQuadlets = append(runningQuadlets, quadletName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logrus.Infof("Going to stop systemd unit %s (Quadlet %s)", unitStatus.Name, quadletName)
|
||||||
|
ch := make(chan string)
|
||||||
|
if _, err := conn.StopUnitContext(ctx, unitStatus.Name, "replace", ch); err != nil {
|
||||||
|
report.Errors[quadletName] = fmt.Errorf("stopping quadlet %s: %w", quadletName, err)
|
||||||
|
runningQuadlets = append(runningQuadlets, quadletName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logrus.Debugf("Waiting for systemd unit %s to stop", unitStatus.Name)
|
||||||
|
stopResult := <-ch
|
||||||
|
if stopResult != "done" && stopResult != "skipped" {
|
||||||
|
report.Errors[quadletName] = fmt.Errorf("unable to stop quadlet %s: %s", quadletName, stopResult)
|
||||||
|
runningQuadlets = append(runningQuadlets, quadletName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the actual files behind the quadlets
|
||||||
|
if len(allQuadletPaths) != 0 {
|
||||||
|
for _, path := range allQuadletPaths {
|
||||||
|
quadletName := filepath.Base(path)
|
||||||
|
if slices.Contains(runningQuadlets, quadletName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
report.Errors[quadletName] = fmt.Errorf("removing quadlet %s: %w", quadletName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report.Removed = append(report.Removed, quadletName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload systemd, if necessary/requested.
|
||||||
|
if needReload {
|
||||||
|
if err := conn.ReloadContext(ctx); err != nil {
|
||||||
|
return &report, fmt.Errorf("reloading systemd: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &report, nil
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errNotImplemented = errors.New("not implemented for the remote Podman client")
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []string, options entities.QuadletInstallOptions) (*entities.QuadletInstallReport, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) QuadletList(ctx context.Context, options entities.QuadletListOptions) ([]*entities.ListQuadlet, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) QuadletPrint(ctx context.Context, quadlet string) (string, error) {
|
||||||
|
return "", errNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ic *ContainerEngine) QuadletRemove(ctx context.Context, quadlets []string, options entities.QuadletRemoveOptions) (*entities.QuadletRemoveReport, error) {
|
||||||
|
return nil, errNotImplemented
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package logiface
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
Debugf(format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger Logger
|
||||||
|
|
||||||
|
func SetLogger(l Logger) {
|
||||||
|
logger = l
|
||||||
|
}
|
||||||
|
|
||||||
|
func Errorf(format string, args ...interface{}) {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debugf(format string, args ...interface{}) {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Debugf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
|
@ -207,18 +207,6 @@ type GroupInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Key: Extension
|
|
||||||
// Value: Processing order for resource naming dependencies
|
|
||||||
SupportedExtensions = map[string]int{
|
|
||||||
".container": 4,
|
|
||||||
".volume": 2,
|
|
||||||
".kube": 4,
|
|
||||||
".network": 2,
|
|
||||||
".image": 1,
|
|
||||||
".build": 3,
|
|
||||||
".pod": 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
URL = regexp.Delayed(`^((https?)|(git)://)|(github\.com/).+$`)
|
URL = regexp.Delayed(`^((https?)|(git)://)|(github\.com/).+$`)
|
||||||
validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`)
|
validPortRange = regexp.Delayed(`\d+(-\d+)?(/udp|/tcp)?$`)
|
||||||
|
|
||||||
|
@ -1449,6 +1437,27 @@ func GetBuiltImageName(buildUnit *parser.UnitFile) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUnitServiceName(unit *parser.UnitFile) (string, error) {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(unit.Filename, ".container"):
|
||||||
|
return GetContainerServiceName(unit), nil
|
||||||
|
case strings.HasSuffix(unit.Filename, ".volume"):
|
||||||
|
return GetVolumeServiceName(unit), nil
|
||||||
|
case strings.HasSuffix(unit.Filename, ".kube"):
|
||||||
|
return GetKubeServiceName(unit), nil
|
||||||
|
case strings.HasSuffix(unit.Filename, ".network"):
|
||||||
|
return GetNetworkServiceName(unit), nil
|
||||||
|
case strings.HasSuffix(unit.Filename, ".image"):
|
||||||
|
return GetImageServiceName(unit), nil
|
||||||
|
case strings.HasSuffix(unit.Filename, ".build"):
|
||||||
|
return GetBuildServiceName(unit), nil
|
||||||
|
case strings.HasSuffix(unit.Filename, ".pod"):
|
||||||
|
return GetPodServiceName(unit), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported file type %q", unit.Filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func GetContainerServiceName(podUnit *parser.UnitFile) string {
|
func GetContainerServiceName(podUnit *parser.UnitFile) string {
|
||||||
return getServiceName(podUnit, ContainerGroup, "")
|
return getServiceName(podUnit, ContainerGroup, "")
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package quadlet
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Key: Extension
|
||||||
|
// Value: Processing order for resource naming dependencies
|
||||||
|
SupportedExtensions = map[string]int{
|
||||||
|
".container": 4,
|
||||||
|
".volume": 2,
|
||||||
|
".kube": 4,
|
||||||
|
".network": 2,
|
||||||
|
".image": 1,
|
||||||
|
".build": 3,
|
||||||
|
".pod": 5,
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,233 @@
|
||||||
|
package quadlet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v5/pkg/logiface"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This returns whether a file has an extension recognized as a valid Quadlet unit type.
|
||||||
|
func IsExtSupported(filename string) bool {
|
||||||
|
ext := filepath.Ext(filename)
|
||||||
|
_, ok := SupportedExtensions[ext]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// This returns the directories where we read quadlet .container and .volumes from
|
||||||
|
// For system generators these are in /usr/share/containers/systemd (for distro files)
|
||||||
|
// and /etc/containers/systemd (for sysadmin files).
|
||||||
|
// For user generators these can live in $XDG_RUNTIME_DIR/containers/systemd, /etc/containers/systemd/users, /etc/containers/systemd/users/$UID, and $XDG_CONFIG_HOME/containers/systemd
|
||||||
|
func GetUnitDirs(rootless bool) []string {
|
||||||
|
paths := NewSearchPaths()
|
||||||
|
|
||||||
|
// Allow overriding source dir, this is mainly for the CI tests
|
||||||
|
if getDirsFromEnv(paths) {
|
||||||
|
return paths.sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedUnitDirAdminUser := ResolveUnitDirAdminUser()
|
||||||
|
userLevelFilter := GetUserLevelFilter(resolvedUnitDirAdminUser)
|
||||||
|
|
||||||
|
if rootless {
|
||||||
|
systemUserDirLevel := len(strings.Split(resolvedUnitDirAdminUser, string(os.PathSeparator)))
|
||||||
|
nonNumericFilter := GetNonNumericFilter(resolvedUnitDirAdminUser, systemUserDirLevel)
|
||||||
|
getRootlessDirs(paths, nonNumericFilter, userLevelFilter)
|
||||||
|
} else {
|
||||||
|
getRootDirs(paths, userLevelFilter)
|
||||||
|
}
|
||||||
|
return paths.sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
type searchPaths struct {
|
||||||
|
sorted []string
|
||||||
|
// map to store paths so we can quickly check if we saw them already and not loop in case of symlinks
|
||||||
|
visitedDirs map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSearchPaths() *searchPaths {
|
||||||
|
return &searchPaths{
|
||||||
|
sorted: make([]string, 0),
|
||||||
|
visitedDirs: make(map[string]struct{}, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *searchPaths) Add(path string) {
|
||||||
|
s.sorted = append(s.sorted, path)
|
||||||
|
s.visitedDirs[path] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *searchPaths) GetSortedPaths() []string {
|
||||||
|
return s.sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *searchPaths) Visited(path string) bool {
|
||||||
|
_, visited := s.visitedDirs[path]
|
||||||
|
return visited
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDirsFromEnv(paths *searchPaths) bool {
|
||||||
|
unitDirsEnv := os.Getenv("QUADLET_UNIT_DIRS")
|
||||||
|
if len(unitDirsEnv) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, eachUnitDir := range strings.Split(unitDirsEnv, ":") {
|
||||||
|
if !filepath.IsAbs(eachUnitDir) {
|
||||||
|
logiface.Errorf("%s not a valid file path", eachUnitDir)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
AppendSubPaths(paths, eachUnitDir, false, nil)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func AppendSubPaths(paths *searchPaths, path string, isUserFlag bool, filterPtr func(string, bool) bool) {
|
||||||
|
resolvedPath, err := filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
logiface.Debugf("Error occurred resolving path %q: %s", path, err)
|
||||||
|
}
|
||||||
|
// Despite the failure add the path to the list for logging purposes
|
||||||
|
// This is the equivalent of adding the path when info==nil below
|
||||||
|
paths.Add(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if skipPath(paths, resolvedPath, isUserFlag, filterPtr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the current directory
|
||||||
|
paths.Add(resolvedPath)
|
||||||
|
|
||||||
|
// Read the contents of the directory
|
||||||
|
entries, err := os.ReadDir(resolvedPath)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
logiface.Debugf("Error occurred walking sub directories %q: %s", path, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively run through the contents of the directory
|
||||||
|
for _, entry := range entries {
|
||||||
|
fullPath := filepath.Join(resolvedPath, entry.Name())
|
||||||
|
AppendSubPaths(paths, fullPath, isUserFlag, filterPtr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipPath(paths *searchPaths, path string, isUserFlag bool, filterPtr func(string, bool) bool) bool {
|
||||||
|
// If the path is already in the map no need to read it again
|
||||||
|
if paths.Visited(path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't traverse drop-in directories
|
||||||
|
if strings.HasSuffix(path, ".d") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the directory should be filtered out
|
||||||
|
if filterPtr != nil && !filterPtr(path, isUserFlag) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
logiface.Debugf("Error occurred resolving path %q: %s", path, err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a directory nothing to add
|
||||||
|
return !stat.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveUnitDirAdminUser() string {
|
||||||
|
unitDirAdminUser := filepath.Join(UnitDirAdmin, "users")
|
||||||
|
var err error
|
||||||
|
var resolvedUnitDirAdminUser string
|
||||||
|
if resolvedUnitDirAdminUser, err = filepath.EvalSymlinks(unitDirAdminUser); err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
logiface.Debugf("Error occurred resolving path %q: %s", unitDirAdminUser, err)
|
||||||
|
}
|
||||||
|
resolvedUnitDirAdminUser = unitDirAdminUser
|
||||||
|
}
|
||||||
|
return resolvedUnitDirAdminUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserLevelFilter(resolvedUnitDirAdminUser string) func(string, bool) bool {
|
||||||
|
return func(_path string, isUserFlag bool) bool {
|
||||||
|
// if quadlet generator is run rootless, do not recurse other user sub dirs
|
||||||
|
// if quadlet generator is run as root, ignore users sub dirs
|
||||||
|
if strings.HasPrefix(_path, resolvedUnitDirAdminUser) {
|
||||||
|
if isUserFlag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNonNumericFilter(resolvedUnitDirAdminUser string, systemUserDirLevel int) func(string, bool) bool {
|
||||||
|
return func(path string, isUserFlag bool) bool {
|
||||||
|
// when running in rootless, recursive walk directories that are non numeric
|
||||||
|
// ignore sub dirs under the `users` directory which correspond to a user id
|
||||||
|
if strings.HasPrefix(path, resolvedUnitDirAdminUser) {
|
||||||
|
listDirUserPathLevels := strings.Split(path, string(os.PathSeparator))
|
||||||
|
// Make sure to add the base directory
|
||||||
|
if len(listDirUserPathLevels) == systemUserDirLevel {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(listDirUserPathLevels) > systemUserDirLevel {
|
||||||
|
if !(regexp.MustCompile(`^[0-9]*$`).MatchString(listDirUserPathLevels[systemUserDirLevel])) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRootlessDirs(paths *searchPaths, nonNumericFilter, userLevelFilter func(string, bool) bool) {
|
||||||
|
runtimeDir, found := os.LookupEnv("XDG_RUNTIME_DIR")
|
||||||
|
if found {
|
||||||
|
AppendSubPaths(paths, path.Join(runtimeDir, "containers/systemd"), false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AppendSubPaths(paths, path.Join(configDir, "containers/systemd"), false, nil)
|
||||||
|
|
||||||
|
u, err := user.Current()
|
||||||
|
if err == nil {
|
||||||
|
AppendSubPaths(paths, filepath.Join(UnitDirAdmin, "users"), true, nonNumericFilter)
|
||||||
|
AppendSubPaths(paths, filepath.Join(UnitDirAdmin, "users", u.Uid), true, userLevelFilter)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: %v", err)
|
||||||
|
// Add the base directory even if the UID was not found
|
||||||
|
paths.Add(filepath.Join(UnitDirAdmin, "users"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRootDirs(paths *searchPaths, userLevelFilter func(string, bool) bool) {
|
||||||
|
AppendSubPaths(paths, UnitDirTemp, false, userLevelFilter)
|
||||||
|
AppendSubPaths(paths, UnitDirAdmin, false, userLevelFilter)
|
||||||
|
AppendSubPaths(paths, UnitDirDistro, false, nil)
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package quadlet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnitDirs(t *testing.T) {
|
||||||
|
u, err := user.Current()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
uidInt, err := strconv.Atoi(u.Uid)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
if os.Getenv("_UNSHARED") != "true" {
|
||||||
|
unitDirs := GetUnitDirs(false)
|
||||||
|
|
||||||
|
resolvedUnitDirAdminUser := ResolveUnitDirAdminUser()
|
||||||
|
userLevelFilter := GetUserLevelFilter(resolvedUnitDirAdminUser)
|
||||||
|
rootfulPaths := NewSearchPaths()
|
||||||
|
AppendSubPaths(rootfulPaths, UnitDirTemp, false, userLevelFilter)
|
||||||
|
AppendSubPaths(rootfulPaths, UnitDirAdmin, false, userLevelFilter)
|
||||||
|
AppendSubPaths(rootfulPaths, UnitDirDistro, false, userLevelFilter)
|
||||||
|
assert.Equal(t, rootfulPaths.GetSortedPaths(), unitDirs, "rootful unit dirs should match")
|
||||||
|
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
rootlessPaths := NewSearchPaths()
|
||||||
|
|
||||||
|
systemUserDirLevel := len(strings.Split(resolvedUnitDirAdminUser, string(os.PathSeparator)))
|
||||||
|
nonNumericFilter := GetNonNumericFilter(resolvedUnitDirAdminUser, systemUserDirLevel)
|
||||||
|
|
||||||
|
runtimeDir, found := os.LookupEnv("XDG_RUNTIME_DIR")
|
||||||
|
if found {
|
||||||
|
AppendSubPaths(rootlessPaths, path.Join(runtimeDir, "containers/systemd"), false, nil)
|
||||||
|
}
|
||||||
|
AppendSubPaths(rootlessPaths, path.Join(configDir, "containers/systemd"), false, nil)
|
||||||
|
AppendSubPaths(rootlessPaths, filepath.Join(UnitDirAdmin, "users"), true, nonNumericFilter)
|
||||||
|
AppendSubPaths(rootlessPaths, filepath.Join(UnitDirAdmin, "users", u.Uid), true, userLevelFilter)
|
||||||
|
|
||||||
|
unitDirs = GetUnitDirs(true)
|
||||||
|
assert.Equal(t, rootlessPaths.GetSortedPaths(), unitDirs, "rootless unit dirs should match")
|
||||||
|
|
||||||
|
// Test that relative path returns an empty list
|
||||||
|
t.Setenv("QUADLET_UNIT_DIRS", "./relative/path")
|
||||||
|
unitDirs = GetUnitDirs(false)
|
||||||
|
assert.Equal(t, []string{}, unitDirs)
|
||||||
|
|
||||||
|
name := t.TempDir()
|
||||||
|
t.Setenv("QUADLET_UNIT_DIRS", name)
|
||||||
|
unitDirs = GetUnitDirs(false)
|
||||||
|
assert.Equal(t, []string{name}, unitDirs, "rootful should use environment variable")
|
||||||
|
|
||||||
|
unitDirs = GetUnitDirs(true)
|
||||||
|
assert.Equal(t, []string{name}, unitDirs, "rootless should use environment variable")
|
||||||
|
|
||||||
|
symLinkTestBaseDir := t.TempDir()
|
||||||
|
|
||||||
|
actualDir := filepath.Join(symLinkTestBaseDir, "actual")
|
||||||
|
err = os.Mkdir(actualDir, 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
innerDir := filepath.Join(actualDir, "inner")
|
||||||
|
err = os.Mkdir(innerDir, 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
symlink := filepath.Join(symLinkTestBaseDir, "symlink")
|
||||||
|
err = os.Symlink(actualDir, symlink)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
t.Setenv("QUADLET_UNIT_DIRS", symlink)
|
||||||
|
unitDirs = GetUnitDirs(true)
|
||||||
|
assert.Equal(t, []string{actualDir, innerDir}, unitDirs, "directory resolution should follow symlink")
|
||||||
|
|
||||||
|
// Make a more elborate test with the following structure:
|
||||||
|
// <BASE>/linkToDir - real directory to link to
|
||||||
|
// <BASE>/linkToDir/a - real directory
|
||||||
|
// <BASE>/linkToDir/b - link to <BASE>/unitDir/b/a should be ignored
|
||||||
|
// <BASE>/linkToDir/c - link to <BASE>/unitDir should be ignored
|
||||||
|
// <BASE>/unitDir - start from here
|
||||||
|
// <BASE>/unitDir/a - real directory
|
||||||
|
// <BASE>/unitDir/a/a - real directory
|
||||||
|
// <BASE>/unitDir/a/a/a - real directory
|
||||||
|
// <BASE>/unitDir/b/a - real directory
|
||||||
|
// <BASE>/unitDir/b/b - link to <BASE>/unitDir/a/a should be ignored
|
||||||
|
// <BASE>/unitDir/c - link to <BASE>/linkToDir
|
||||||
|
createDir := func(path, name string, dirs []string) (string, []string) {
|
||||||
|
dirName := filepath.Join(path, name)
|
||||||
|
assert.NotContains(t, dirs, dirName)
|
||||||
|
err = os.Mkdir(dirName, 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
dirs = append(dirs, dirName)
|
||||||
|
return dirName, dirs
|
||||||
|
}
|
||||||
|
|
||||||
|
linkDir := func(path, name, target string) {
|
||||||
|
linkName := filepath.Join(path, name)
|
||||||
|
err = os.Symlink(target, linkName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
symLinkRecursiveTestBaseDir := t.TempDir()
|
||||||
|
|
||||||
|
expectedDirs := make([]string, 0)
|
||||||
|
// Create <BASE>/unitDir
|
||||||
|
unitsDirPath, expectedDirs := createDir(symLinkRecursiveTestBaseDir, "unitsDir", expectedDirs)
|
||||||
|
// Create <BASE>/unitDir/a
|
||||||
|
aDirPath, expectedDirs := createDir(unitsDirPath, "a", expectedDirs)
|
||||||
|
// Create <BASE>/unitDir/a/a
|
||||||
|
aaDirPath, expectedDirs := createDir(aDirPath, "a", expectedDirs)
|
||||||
|
// Create <BASE>/unitDir/a/a/a
|
||||||
|
_, expectedDirs = createDir(aaDirPath, "a", expectedDirs)
|
||||||
|
// Create <BASE>/unitDir/a/b
|
||||||
|
_, expectedDirs = createDir(aDirPath, "b", expectedDirs)
|
||||||
|
// Create <BASE>/unitDir/b
|
||||||
|
bDirPath, expectedDirs := createDir(unitsDirPath, "b", expectedDirs)
|
||||||
|
// Create <BASE>/unitDir/b/a
|
||||||
|
baDirPath, expectedDirs := createDir(bDirPath, "a", expectedDirs)
|
||||||
|
// Create <BASE>/linkToDir
|
||||||
|
linkToDirPath, expectedDirs := createDir(symLinkRecursiveTestBaseDir, "linkToDir", expectedDirs)
|
||||||
|
// Create <BASE>/linkToDir/a
|
||||||
|
_, expectedDirs = createDir(linkToDirPath, "a", expectedDirs)
|
||||||
|
|
||||||
|
// Link <BASE>/unitDir/b/b to <BASE>/unitDir/a/a
|
||||||
|
linkDir(bDirPath, "b", aaDirPath)
|
||||||
|
// Link <BASE>/linkToDir/b to <BASE>/unitDir/b/a
|
||||||
|
linkDir(linkToDirPath, "b", baDirPath)
|
||||||
|
// Link <BASE>/linkToDir/c to <BASE>/unitDir
|
||||||
|
linkDir(linkToDirPath, "c", unitsDirPath)
|
||||||
|
// Link <BASE>/unitDir/c to <BASE>/linkToDir
|
||||||
|
linkDir(unitsDirPath, "c", linkToDirPath)
|
||||||
|
|
||||||
|
t.Setenv("QUADLET_UNIT_DIRS", unitsDirPath)
|
||||||
|
unitDirs = GetUnitDirs(true)
|
||||||
|
assert.Equal(t, expectedDirs, unitDirs, "directory resolution should follow symlink")
|
||||||
|
// remove the temporary directory at the end of the program
|
||||||
|
defer os.RemoveAll(symLinkTestBaseDir)
|
||||||
|
|
||||||
|
// because chroot is only available for root,
|
||||||
|
// unshare the namespace and map user to root
|
||||||
|
c := exec.Command("/proc/self/exe", os.Args[1:]...)
|
||||||
|
c.Stdin = os.Stdin
|
||||||
|
c.Stdout = os.Stdout
|
||||||
|
c.Stderr = os.Stderr
|
||||||
|
c.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Cloneflags: syscall.CLONE_NEWUSER,
|
||||||
|
UidMappings: []syscall.SysProcIDMap{
|
||||||
|
{
|
||||||
|
ContainerID: 0,
|
||||||
|
HostID: uidInt,
|
||||||
|
Size: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.Env = append(os.Environ(), "_UNSHARED=true")
|
||||||
|
err = c.Run()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
fmt.Println(os.Args)
|
||||||
|
|
||||||
|
symLinkTestBaseDir := t.TempDir()
|
||||||
|
rootF, err := os.Open("/")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer rootF.Close()
|
||||||
|
defer func() {
|
||||||
|
err := rootF.Chdir()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = syscall.Chroot(".")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}()
|
||||||
|
err = syscall.Chroot(symLinkTestBaseDir)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.MkdirAll(UnitDirAdmin, 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = os.RemoveAll(UnitDirAdmin)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
createDir := func(path, name string) string {
|
||||||
|
dirName := filepath.Join(path, name)
|
||||||
|
err = os.Mkdir(dirName, 0755)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return dirName
|
||||||
|
}
|
||||||
|
|
||||||
|
linkDir := func(path, name, target string) {
|
||||||
|
linkName := filepath.Join(path, name)
|
||||||
|
err = os.Symlink(target, linkName)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemdDir := createDir("/", "systemd")
|
||||||
|
userDir := createDir("/", "users")
|
||||||
|
linkDir(systemdDir, "users", userDir)
|
||||||
|
linkDir(UnitDirAdmin, "", systemdDir)
|
||||||
|
|
||||||
|
uidDir := createDir(userDir, u.Uid)
|
||||||
|
uidDir2 := createDir(userDir, strconv.Itoa(uidInt+1))
|
||||||
|
userInternalDir := createDir(userDir, "internal")
|
||||||
|
|
||||||
|
// Make sure QUADLET_UNIT_DIRS is not set
|
||||||
|
t.Setenv("QUADLET_UNIT_DIRS", "")
|
||||||
|
// Test Rootful
|
||||||
|
unitDirs := GetUnitDirs(false)
|
||||||
|
assert.NotContains(t, unitDirs, userDir, "rootful should not contain rootless")
|
||||||
|
assert.NotContains(t, unitDirs, userInternalDir, "rootful should not contain rootless")
|
||||||
|
|
||||||
|
// Test Rootless
|
||||||
|
unitDirs = GetUnitDirs(true)
|
||||||
|
assert.NotContains(t, unitDirs, uidDir2, "rootless should not contain other users'")
|
||||||
|
assert.Contains(t, unitDirs, userInternalDir, "rootless should contain sub-directories of users dir")
|
||||||
|
assert.Contains(t, unitDirs, uidDir, "rootless should contain the directory for its UID")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1865,4 +1865,164 @@ EOF
|
||||||
|
|
||||||
run_podman rmi -i $image_tag
|
run_podman rmi -i $image_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "quadlet verb - install, list, rm" {
|
||||||
|
# Create a test quadlet file
|
||||||
|
local quadlet_file=$PODMAN_TMPDIR/alpine-quadlet.container
|
||||||
|
cat > $quadlet_file <<EOF
|
||||||
|
[Container]
|
||||||
|
Image=$IMAGE
|
||||||
|
Exec=sh -c "echo STARTED CONTAINER; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
|
||||||
|
Notify=yes
|
||||||
|
LogDriver=passthrough
|
||||||
|
EOF
|
||||||
|
# Clean all quadlets
|
||||||
|
run_podman quadlet rm --all -f
|
||||||
|
assert $status -eq 0 "all quadlets should be cleaned"
|
||||||
|
|
||||||
|
# Test quadlet install
|
||||||
|
run_podman quadlet install $quadlet_file
|
||||||
|
assert $status -eq 0 "quadlet install should succeed"
|
||||||
|
|
||||||
|
# Test quadlet list
|
||||||
|
run_podman quadlet list
|
||||||
|
assert $status -eq 0 "quadlet list should succeed"
|
||||||
|
assert "$output" =~ "alpine-quadlet.container" "list should contain alpine-quadlet.container"
|
||||||
|
|
||||||
|
# Test quadlet list with filter
|
||||||
|
run_podman quadlet list --filter name=something*
|
||||||
|
assert $status -eq 0 "quadlet list with filter should succeed"
|
||||||
|
assert "$output" !~ "alpine-quadlet.container" "filtered list should not contain alpine-quadlet.container"
|
||||||
|
|
||||||
|
# Test quadlet list with matching filter
|
||||||
|
run_podman quadlet list --filter name=alpine*
|
||||||
|
assert $status -eq 0 "quadlet list with matching filter should succeed"
|
||||||
|
assert "$output" =~ "alpine-quadlet.container" "matching filter should contain alpine-quadlet.container"
|
||||||
|
|
||||||
|
# Test quadlet print
|
||||||
|
run_podman quadlet print alpine-quadlet.container
|
||||||
|
assert $status -eq 0 "quadlet print should succeed"
|
||||||
|
assert "$output" =~ "\[Container\]" "print should show container section"
|
||||||
|
assert "$output" =~ "Image=$IMAGE" "print should show correct image"
|
||||||
|
|
||||||
|
# Test quadlet rm
|
||||||
|
run_podman quadlet rm alpine-quadlet.container
|
||||||
|
assert $status -eq 0 "quadlet rm should succeed"
|
||||||
|
|
||||||
|
# Verify removal
|
||||||
|
run_podman quadlet list
|
||||||
|
assert $status -eq 0 "quadlet list after removal should succeed"
|
||||||
|
assert "$output" !~ "alpine-quadlet.container" "list should not contain removed container"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bats test_tags=distro-integration
|
||||||
|
@test "quadlet verb - install multiple files from directory" {
|
||||||
|
# Create a directory for multiple quadlet files
|
||||||
|
local quadlet_dir=$PODMAN_TMPDIR/quadlet-multi
|
||||||
|
mkdir -p $quadlet_dir
|
||||||
|
|
||||||
|
# Create multiple quadlet files with different configurations
|
||||||
|
cat > $quadlet_dir/alpine1.container <<EOF
|
||||||
|
[Container]
|
||||||
|
Image=$IMAGE
|
||||||
|
Exec=sh -c "echo STARTED CONTAINER 1; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
|
||||||
|
Notify=yes
|
||||||
|
LogDriver=passthrough
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > $quadlet_dir/alpine2.container <<EOF
|
||||||
|
[Container]
|
||||||
|
Image=$IMAGE
|
||||||
|
Exec=sh -c "echo STARTED CONTAINER 2; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
|
||||||
|
Notify=yes
|
||||||
|
LogDriver=passthrough
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > $quadlet_dir/nginx.container <<EOF
|
||||||
|
[Container]
|
||||||
|
Image=quay.io/libpod/nginx:latest
|
||||||
|
Exec=sh -c "echo STARTED NGINX; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
|
||||||
|
Notify=yes
|
||||||
|
LogDriver=passthrough
|
||||||
|
EOF
|
||||||
|
# Clean all quadlets
|
||||||
|
run_podman quadlet rm --all -f
|
||||||
|
assert $status -eq 0 "all quadlets should be cleaned"
|
||||||
|
|
||||||
|
# Test quadlet install with directory
|
||||||
|
run_podman quadlet install $quadlet_dir
|
||||||
|
assert $status -eq 0 "quadlet install with directory should succeed"
|
||||||
|
|
||||||
|
# Test quadlet list to verify all containers were installed
|
||||||
|
run_podman quadlet list
|
||||||
|
assert $status -eq 0 "quadlet list should succeed"
|
||||||
|
assert "$output" =~ "alpine1.container" "list should contain alpine1.container"
|
||||||
|
assert "$output" =~ "alpine2.container" "list should contain alpine2.container"
|
||||||
|
assert "$output" =~ "nginx.container" "list should contain nginx.container"
|
||||||
|
|
||||||
|
# Test quadlet list with filter for alpine containers
|
||||||
|
run_podman quadlet list --filter name=alpine*
|
||||||
|
assert $status -eq 0 "quadlet list with filter should succeed"
|
||||||
|
assert "$output" =~ "alpine1.container" "filtered list should contain alpine1.container"
|
||||||
|
assert "$output" =~ "alpine2.container" "filtered list should contain alpine2.container"
|
||||||
|
assert "$output" !~ "nginx.container" "filtered list should not contain nginx.container"
|
||||||
|
|
||||||
|
# Test quadlet print for each container
|
||||||
|
run_podman quadlet print alpine1.container
|
||||||
|
assert $status -eq 0 "quadlet print should succeed for alpine1"
|
||||||
|
assert "$output" =~ "Image=$IMAGE" "print should show correct image for alpine1"
|
||||||
|
|
||||||
|
run_podman quadlet print alpine2.container
|
||||||
|
assert $status -eq 0 "quadlet print should succeed for alpine2"
|
||||||
|
assert "$output" =~ "Image=$IMAGE" "print should show correct image for alpine2"
|
||||||
|
|
||||||
|
run_podman quadlet print nginx.container
|
||||||
|
assert $status -eq 0 "quadlet print should succeed for nginx"
|
||||||
|
assert "$output" =~ "Image=quay.io/libpod/nginx:latest" "print should show correct image for nginx"
|
||||||
|
|
||||||
|
# Test quadlet rm for all containers
|
||||||
|
run_podman quadlet rm alpine1.container alpine2.container nginx.container
|
||||||
|
assert $status -eq 0 "quadlet rm should succeed for all containers"
|
||||||
|
|
||||||
|
# Verify all containers were removed
|
||||||
|
run_podman quadlet list
|
||||||
|
assert $status -eq 0 "quadlet list after removal should succeed"
|
||||||
|
assert "$output" !~ "alpine1.container" "list should not contain removed container alpine1"
|
||||||
|
assert "$output" !~ "alpine2.container" "list should not contain removed container alpine2"
|
||||||
|
assert "$output" !~ "nginx.container" "list should not contain removed container nginx"
|
||||||
|
}
|
||||||
|
|
||||||
|
# bats test_tags=distro-integration
|
||||||
|
@test "quadlet verb - install from URL" {
|
||||||
|
# Clean all quadlets
|
||||||
|
run_podman quadlet rm --all -f
|
||||||
|
assert $status -eq 0 "all quadlets should be cleaned"
|
||||||
|
|
||||||
|
# Test quadlet install from URL and capture the output
|
||||||
|
run_podman quadlet install https://raw.githubusercontent.com/containers/podman/main/test/e2e/quadlet/basic.container
|
||||||
|
assert $status -eq 0 "quadlet install from URL should succeed"
|
||||||
|
# Extract just the basename from the full path
|
||||||
|
quadlet_name=$(basename "$output")
|
||||||
|
|
||||||
|
# Test quadlet list to verify the container was installed
|
||||||
|
run_podman quadlet list
|
||||||
|
assert $status -eq 0 "quadlet list should succeed"
|
||||||
|
assert "$output" =~ "$quadlet_name" "list should contain $quadlet_name"
|
||||||
|
|
||||||
|
# Test quadlet print to verify the configuration
|
||||||
|
run_podman quadlet print "$quadlet_name"
|
||||||
|
assert $status -eq 0 "quadlet print should succeed"
|
||||||
|
assert "$output" =~ "\[Container\]" "print should show container section"
|
||||||
|
assert "$output" =~ "Image=" "print should show image configuration"
|
||||||
|
|
||||||
|
# Test quadlet rm
|
||||||
|
run_podman quadlet rm "$quadlet_name"
|
||||||
|
assert $status -eq 0 "quadlet rm should succeed"
|
||||||
|
|
||||||
|
# Verify removal
|
||||||
|
run_podman quadlet list
|
||||||
|
assert $status -eq 0 "quadlet list after removal should succeed"
|
||||||
|
assert "$output" !~ "$quadlet_name" "list should not contain removed container"
|
||||||
|
}
|
||||||
|
|
||||||
# vim: filetype=sh
|
# vim: filetype=sh
|
||||||
|
|
Loading…
Reference in New Issue