Skip to content

Add support for podman quadlet #26330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions cmd/podman/common/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,28 @@ func getPods(cmd *cobra.Command, toComplete string, cType completeType, statuses
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) {
suggestions := []string{}
lsOpts := entities.VolumeListOptions{}
Expand Down Expand Up @@ -730,6 +752,14 @@ func AutocompleteImages(cmd *cobra.Command, args []string, toComplete string) ([
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.
func AutocompleteManifestListAndMember(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {
Expand Down Expand Up @@ -827,6 +857,11 @@ func AutocompleteDefaultOneArg(cmd *cobra.Command, args []string, toComplete str
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.
func AutocompleteCommitCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if !validCurrentCmdLine(cmd, args, toComplete) {
Expand Down Expand Up @@ -1775,6 +1810,14 @@ func AutocompletePsFilters(cmd *cobra.Command, args []string, toComplete string)
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.
func AutocompletePodPsFilters(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
kv := keyValueCompletion{
Expand Down
12 changes: 12 additions & 0 deletions cmd/podman/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import (
_ "github.com/containers/podman/v5/cmd/podman/manifest"
_ "github.com/containers/podman/v5/cmd/podman/networks"
_ "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/secrets"
_ "github.com/containers/podman/v5/cmd/podman/system"
_ "github.com/containers/podman/v5/cmd/podman/system/connection"
"github.com/containers/podman/v5/cmd/podman/validate"
_ "github.com/containers/podman/v5/cmd/podman/volumes"
"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/terminal"
"github.com/containers/storage/pkg/reexec"
Expand All @@ -34,12 +36,22 @@ import (
"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() {
if reexec.Init() {
// We were invoked with a different argv[0] indicating that we
// had a specific job to do as a subprocess, and it's done.
return
}
logiface.SetLogger(logrusLogger{})

if filepath.Base(os.Args[0]) == registry.PodmanSh ||
(len(os.Args[0]) > 0 && filepath.Base(os.Args[0][1:]) == registry.PodmanSh) {
Expand Down
68 changes: 68 additions & 0 deletions cmd/podman/quadlet/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 a quadlet file or quadlet application. Quadlets may be specified as local files, Web URLs, and OCI artifacts.`

quadletInstallCmd = &cobra.Command{
Use: "install [options] QUADLET-PATH-OR-URL [FILES-PATH-OR-URL...]",
Short: "Install a quadlet file or quadlet application",
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
Copy link
Contributor

Choose a reason for hiding this comment

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

question: what would be the recommend way of installing a kube quadlet? since the YAML would not be copied with this command?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, this is an even bigger questions. Many .container units come with a configuration file stored locally and mounted into the container. For example, see: https://github.com/containers/appstore/tree/main/quadlet/minio-prometheus. prometheus.yml must be copied along with prometheus.container.

Furthermore, if you look at the examples, you can see that in many cases, more than one file is needed (units and others). Can the install command support installing a directory?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Furthermore, if you look at the examples, you can see that in many cases, more than one file is needed (units and others). Can the install command support installing a directory?

Yes it supports multiplefile but I think we need to start consuming entire directory as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes it supports multiplefile but I think we need to start consuming entire directory as well.

Yes, having to list all the files does not make sense for the user. Plus, as I wrote not all of them will be Quadlet files

Copy link
Contributor

Choose a reason for hiding this comment

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

If we copy resources, we may want to ensure they don't already exists, and have a --force flag, to allow overwriting existing files

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have added patch to install from directory in new commit.

If we copy resources, we may want to ensure they don't already exists, and have a --force flag, to allow overwriting existing files

Will add --force in a follow up PR once this gets merged in order to keep review simpler and not to added too much to matt's original patch.

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()
}
103 changes: 103 additions & 0 deletions cmd/podman/quadlet/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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}}\t{{.App}}\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",
"App": "APPLICATION",
})

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)
}
Comment on lines +78 to +84
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var err error
switch {
case cmd.Flag("format").Changed:
rpt, err = rpt.Parse(report.OriginUser, format)
default:
rpt, err = rpt.Parse(report.OriginPodman, format)
}
origin := report.OriginPodman
if cmd.Flag("format").Changed {
origin = report.OriginUser
}
rpt, err := rpt.Parse(origin, 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
}
43 changes: 43 additions & 0 deletions cmd/podman/quadlet/print.go
Original file line number Diff line number Diff line change
@@ -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
}
26 changes: 26 additions & 0 deletions cmd/podman/quadlet/quadlet.go
Original file line number Diff line number Diff line change
@@ -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,
})
}
Loading