Skip to content
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
270 changes: 270 additions & 0 deletions cmd/podman/kube/quadlet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package kube

import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"
"go.podman.io/common/pkg/completion"
v1 "go.podman.io/podman/v6/pkg/k8s.io/api/core/v1"
"go.podman.io/podman/v6/pkg/kube/quadlet"
"sigs.k8s.io/yaml"

"go.podman.io/podman/v6/cmd/podman/registry"
"go.podman.io/podman/v6/cmd/podman/validate"
)

// quadletJSON is the element type for --format json output.
type quadletJSON struct {
Name string `json:"name"`
Content string `json:"content"`
}

var (
quadletOptions = struct {
OutputDir string
Format string
NamePrefix string
Network string
ConfigMaps []string
Secrets []string
}{}

kubeQuadletCmd = &cobra.Command{
Use: "quadlet [options] POD_YAML",
Short: "Convert a Kubernetes Pod YAML into Quadlet unit files",
Long: `Reads a Kubernetes Pod YAML file and generates the corresponding Quadlet
unit files (.pod, .container, .volume, .env, .sh) without connecting to a Podman daemon.

A .pod unit is always generated so that all containers share a proper infra
container (like a Kubernetes pause container) rather than one container acting
as an anchor for the others.

The output files can be written to a directory or printed to stdout. Use
--format json for programmatic consumption.`,
RunE: runKubeQuadlet,
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.AutocompleteDefault,
// Override the root PersistentPreRunE so no daemon/storage setup is needed.
PersistentPreRunE: validate.NoOp,
Example: `podman kube quadlet pod.yaml
podman kube quadlet --output-dir /etc/containers/systemd pod.yaml
podman kube quadlet --format json pod.yaml | jq .[].name`,
}
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: kubeQuadletCmd,
Parent: kubeCmd,
})

flags := kubeQuadletCmd.Flags()

outputDirFlagName := "output-dir"
flags.StringVarP(&quadletOptions.OutputDir, outputDirFlagName, "o", "", "Write generated files to this directory (default: print to stdout)")
_ = kubeQuadletCmd.RegisterFlagCompletionFunc(outputDirFlagName, completion.AutocompleteDefault)

formatFlagName := "format"
flags.StringVar(&quadletOptions.Format, formatFlagName, "text", `Output format: "text" or "json"`)
_ = kubeQuadletCmd.RegisterFlagCompletionFunc(formatFlagName, completion.AutocompleteNone)

namePrefixFlagName := "name-prefix"
flags.StringVar(&quadletOptions.NamePrefix, namePrefixFlagName, "", "Override the name prefix for generated files (default: pod.metadata.name)")
_ = kubeQuadletCmd.RegisterFlagCompletionFunc(namePrefixFlagName, completion.AutocompleteNone)

networkFlagName := "network"
flags.StringVar(&quadletOptions.Network, networkFlagName, "", "Network= value for the pod unit (e.g. podman, host)")
_ = kubeQuadletCmd.RegisterFlagCompletionFunc(networkFlagName, completion.AutocompleteNone)

configmapFlagName := "configmap"
flags.StringArrayVar(&quadletOptions.ConfigMaps, configmapFlagName, nil, "Path to a ConfigMap YAML file (repeatable)")
_ = kubeQuadletCmd.RegisterFlagCompletionFunc(configmapFlagName, completion.AutocompleteDefault)

secretFlagName := "secret"
flags.StringArrayVar(&quadletOptions.Secrets, secretFlagName, nil, "Path to a Secret YAML file (repeatable)")
_ = kubeQuadletCmd.RegisterFlagCompletionFunc(secretFlagName, completion.AutocompleteDefault)
}

func runKubeQuadlet(_ *cobra.Command, args []string) error {
if quadletOptions.Format != "text" && quadletOptions.Format != "json" {
return fmt.Errorf("--format must be \"text\" or \"json\", got %q", quadletOptions.Format)
}

pod, err := podFromFile(args[0])
if err != nil {
return err
}

opts, err := buildQuadletOptions()
if err != nil {
return err
}

files, err := quadlet.Convert(pod, opts)
if err != nil {
return fmt.Errorf("converting pod to Quadlet: %w", err)
}

if quadletOptions.OutputDir != "" {
return writeFiles(files, quadletOptions.OutputDir)
}

switch quadletOptions.Format {
case "json":
return printJSON(files)
default:
return printText(files)
}
}

// podFromFile reads and unmarshals a Kubernetes Pod from a YAML file.
func podFromFile(path string) (*v1.Pod, error) {
raw, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %q: %w", path, err)
}

// Detect kind before committing to Pod decode.
var meta struct {
Kind string `json:"kind"`
}
if err := yaml.Unmarshal(raw, &meta); err != nil {
return nil, fmt.Errorf("parsing YAML from %q: %w", path, err)
}
if meta.Kind != "Pod" {
return nil, fmt.Errorf("%q has kind %q; only \"Pod\" is supported", path, meta.Kind)
}

var pod v1.Pod
if err := yaml.Unmarshal(raw, &pod); err != nil {
return nil, fmt.Errorf("parsing Pod from %q: %w", path, err)
}
return &pod, nil
}

// buildQuadletOptions constructs Options from CLI flags and any provided
// ConfigMap/Secret YAML files.
func buildQuadletOptions() (quadlet.Options, error) {
opts := quadlet.Options{
NamePrefix: quadletOptions.NamePrefix,
Network: quadletOptions.Network,
}

if len(quadletOptions.ConfigMaps) > 0 {
opts.ConfigMaps = make(map[string]v1.ConfigMap)
for _, cmPath := range quadletOptions.ConfigMaps {
cm, err := loadConfigMap(cmPath)
if err != nil {
return opts, err
}
opts.ConfigMaps[cm.Name] = cm
}
}

if len(quadletOptions.Secrets) > 0 {
opts.Secrets = make(map[string]v1.Secret)
for _, secPath := range quadletOptions.Secrets {
sec, err := loadSecret(secPath)
if err != nil {
return opts, err
}
opts.Secrets[sec.Name] = sec
}
}

return opts, nil
}

func loadConfigMap(path string) (v1.ConfigMap, error) {
raw, err := os.ReadFile(path)
if err != nil {
return v1.ConfigMap{}, fmt.Errorf("reading ConfigMap %q: %w", path, err)
}
var cm v1.ConfigMap
if err := yaml.Unmarshal(raw, &cm); err != nil {
return v1.ConfigMap{}, fmt.Errorf("parsing ConfigMap %q: %w", path, err)
}
return cm, nil
}

func loadSecret(path string) (v1.Secret, error) {
raw, err := os.ReadFile(path)
if err != nil {
return v1.Secret{}, fmt.Errorf("reading Secret %q: %w", path, err)
}
var sec v1.Secret
if err := yaml.Unmarshal(raw, &sec); err != nil {
return v1.Secret{}, fmt.Errorf("parsing Secret %q: %w", path, err)
}
return sec, nil
}

// writeFiles writes each GeneratedFile to outputDir.
func writeFiles(files []*quadlet.GeneratedFile, outputDir string) error {
if err := os.MkdirAll(outputDir, 0o755); err != nil {
return fmt.Errorf("creating output directory %q: %w", outputDir, err)
}
for _, f := range files {
dest := filepath.Join(outputDir, f.Name)
var content []byte
if f.Unit != nil {
var buf bytes.Buffer
if err := f.Unit.Write(&buf); err != nil {
return fmt.Errorf("serializing %q: %w", f.Name, err)
}
content = buf.Bytes()
} else {
content = []byte(f.Content)
}
if err := os.WriteFile(dest, content, 0o644); err != nil {
return fmt.Errorf("writing %q: %w", dest, err)
}
fmt.Println(dest)
}
return nil
}

// printText prints each file separated by a header line.
func printText(files []*quadlet.GeneratedFile) error {
for i, f := range files {
if i > 0 {
fmt.Println()
}
fmt.Printf("=== %s ===\n", f.Name)
if f.Unit != nil {
var buf bytes.Buffer
if err := f.Unit.Write(&buf); err != nil {
return fmt.Errorf("serializing %q: %w", f.Name, err)
}
fmt.Print(buf.String())
} else {
fmt.Print(f.Content)
}
}
return nil
}

// printJSON emits a JSON array of {name, content} objects.
func printJSON(files []*quadlet.GeneratedFile) error {
out := make([]quadletJSON, 0, len(files))
for _, f := range files {
var content string
if f.Unit != nil {
var buf bytes.Buffer
if err := f.Unit.Write(&buf); err != nil {
return fmt.Errorf("serializing %q: %w", f.Name, err)
}
content = buf.String()
} else {
content = f.Content
}
out = append(out, quadletJSON{Name: f.Name, Content: content})
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(out)
}
80 changes: 80 additions & 0 deletions docs/source/markdown/podman-kube-quadlet.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
% podman-kube-quadlet 1

## NAME
podman\-kube\-quadlet - Convert a Kubernetes Pod YAML into Quadlet unit files

## SYNOPSIS
**podman kube quadlet** [*options*] *pod-yaml*

## DESCRIPTION
**podman kube quadlet** reads a Kubernetes Pod YAML file and generates the
corresponding Quadlet unit files (`.pod`, `.container`, `.volume`, `.env`,
`.sh`) without connecting to a Podman daemon. The generated files can be placed
in a systemd user unit directory (e.g. `~/.config/containers/systemd/`) so that
**podman-systemd-generator(8)** starts and manages the pod as a systemd service.

Each container in the Pod becomes a `.container` unit that joins the `.pod` unit
via `Pod=`. Init containers are emitted before regular containers and are given
`Restart=no`. Volume sources are mapped as follows:

- `PersistentVolumeClaim` → `Volume=` (or `Mount=type=volume,subpath=...` when SubPath is set)
- `EmptyDir` → a named tmpfs volume shared across all containers in the pod
- `HostPath` → `Volume=` with `:z` SELinux relabeling
- `Image` → `Mount=type=image`

## OPTIONS

#### **--configmap**=*path*

Path to a Kubernetes ConfigMap YAML file. May be specified multiple times.
ConfigMap volumes in the Pod spec are written as read-only bind-mount directories
containing the key/value pairs.

#### **--format**=**text** | **json**

Output format. **text** (default) writes unit files to the directory given by
**--output-dir** or, if omitted, prints them to standard output. **json** emits
a JSON array where each element has `name` (filename) and `content` (unit file
text) fields; useful for programmatic consumption.

#### **--name-prefix**=*prefix*

Override the name prefix used for all generated unit files. Defaults to the Pod
metadata name.

#### **--network**=*network*

Network to attach the pod to (passed as `Network=` in the `.pod` unit).

#### **--output-dir**, **-o**=*dir*

Directory to write the generated unit files into. If omitted, files are printed
to standard output (one after another, each preceded by a `# filename` comment).

#### **--script-dir**=*dir*

Directory in which companion `.sh` scripts are written when a container's
command is a shell one-liner (`/bin/sh -c <script>`). Defaults to the same
directory as **--output-dir**.

#### **--secret**=*path*

Path to a Kubernetes Secret YAML file. May be specified multiple times.

## EXAMPLES

Convert a pod YAML and write unit files to a systemd user directory.
```
$ podman kube quadlet --output-dir ~/.config/containers/systemd/myapp pod.yaml
```

Convert and emit JSON for programmatic consumption.
```
$ podman kube quadlet --format json pod.yaml | jq '.[].name'
```

## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-kube(1)](podman-kube.1.md)**, **[podman-kube-play(1)](podman-kube-play.1.md)**, **[podman-systemd.unit(5)](podman-systemd.unit.5.md)**

## HISTORY
June 2026, Originally compiled by Asaf Ben Natan (asafbennatan at gmail dot com)
3 changes: 2 additions & 1 deletion docs/source/markdown/podman-kube.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ are deployed to a Kubernetes cluster from podman, please use `kubectl` to manage
| down | [podman-kube-down(1)](podman-kube-down.1.md) | Remove containers and pods based on Kubernetes YAML. |
| generate | [podman-kube-generate(1)](podman-kube-generate.1.md) | Generate Kubernetes YAML based on containers, pods or volumes. |
| play | [podman-kube-play(1)](podman-kube-play.1.md) | Create containers, pods and volumes based on Kubernetes YAML. |
| quadlet | [podman-kube-quadlet(1)](podman-kube-quadlet.1.md) | Convert a Kubernetes Pod YAML into Quadlet unit files. |

## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[podman-kube-play(1)](podman-kube-play.1.md)**, **[podman-kube-down(1)](podman-kube-down.1.md)**, **[podman-kube-generate(1)](podman-kube-generate.1.md)**, **[podman-kube-apply(1)](podman-kube-apply.1.md)**
**[podman(1)](podman.1.md)**, **[podman-pod(1)](podman-pod.1.md)**, **[podman-container(1)](podman-container.1.md)**, **[podman-kube-play(1)](podman-kube-play.1.md)**, **[podman-kube-down(1)](podman-kube-down.1.md)**, **[podman-kube-generate(1)](podman-kube-generate.1.md)**, **[podman-kube-apply(1)](podman-kube-apply.1.md)**, **[podman-kube-quadlet(1)](podman-kube-quadlet.1.md)**

## HISTORY
December 2018, Originally compiled by Brent Baude (bbaude at redhat dot com)
1 change: 1 addition & 0 deletions hack/xref-helpmsgs-manpages
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ my %Format_Option_Is_Special = map { $_ => 1 } (
'commit', 'container commit', # " " " "
'diff', 'container diff', 'image diff', # only supports "json"
'generate systemd', # " " " "
'kube quadlet', # " " " "
'mount', 'container mount', 'image mount', # " " " "
'machine os upgrade', # " " " "
'push', 'image push', 'manifest push', # oci | v2s*
Expand Down
Loading