Merge pull request #25225 from mheon/bump_540_rc3

[CI:ALL] Bump to v5.4.0-RC3
This commit is contained in:
openshift-merge-bot[bot] 2025-02-05 19:00:15 +00:00 committed by GitHub
commit 582d7185df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 587 additions and 162 deletions

View File

@ -6,7 +6,7 @@ env:
#### Global variables used for all tasks #### Global variables used for all tasks
#### ####
# Name of the ultimate destination branch for this CI run, PR or post-merge. # Name of the ultimate destination branch for this CI run, PR or post-merge.
DEST_BRANCH: "main" DEST_BRANCH: "v5.4"
# Sane (default) value for GOPROXY and GOSUMDB. # Sane (default) value for GOPROXY and GOSUMDB.
GOPROXY: "https://proxy.golang.org,direct" GOPROXY: "https://proxy.golang.org,direct"
GOSUMDB: "sum.golang.org" GOSUMDB: "sum.golang.org"

View File

@ -119,6 +119,7 @@ GOFLAGS ?= -trimpath
LDFLAGS_PODMAN ?= \ LDFLAGS_PODMAN ?= \
$(if $(GIT_COMMIT),-X $(LIBPOD)/define.gitCommit=$(GIT_COMMIT),) \ $(if $(GIT_COMMIT),-X $(LIBPOD)/define.gitCommit=$(GIT_COMMIT),) \
$(if $(BUILD_INFO),-X $(LIBPOD)/define.buildInfo=$(BUILD_INFO),) \ $(if $(BUILD_INFO),-X $(LIBPOD)/define.buildInfo=$(BUILD_INFO),) \
$(if $(BUILD_ORIGIN),-X $(LIBPOD)/define.buildOrigin=$(BUILD_ORIGIN),) \
-X $(LIBPOD)/config._installPrefix=$(PREFIX) \ -X $(LIBPOD)/config._installPrefix=$(PREFIX) \
-X $(LIBPOD)/config._etcDir=$(ETCDIR) \ -X $(LIBPOD)/config._etcDir=$(ETCDIR) \
-X $(PROJECT)/v5/pkg/systemd/quadlet._binDir=$(BINDIR) \ -X $(PROJECT)/v5/pkg/systemd/quadlet._binDir=$(BINDIR) \

View File

@ -7,6 +7,7 @@
- The `--mount type=volume` option for the `podman run`, `podman create`, and `podman volume create` commands now supports a new option, `subpath=`, to make only a subset of the volume visible in the container ([#20661](https://github.com/containers/podman/issues/20661)). - The `--mount type=volume` option for the `podman run`, `podman create`, and `podman volume create` commands now supports a new option, `subpath=`, to make only a subset of the volume visible in the container ([#20661](https://github.com/containers/podman/issues/20661)).
- The `--userns=keep-id` option for the `podman run`, `podman create`, and `podman pod create` commands now supports a new option, `--userns=keep-id:size=`, to configure the size of the user namespace ([#24387](https://github.com/containers/podman/issues/24837)). - The `--userns=keep-id` option for the `podman run`, `podman create`, and `podman pod create` commands now supports a new option, `--userns=keep-id:size=`, to configure the size of the user namespace ([#24387](https://github.com/containers/podman/issues/24837)).
- The `podman kube play` command now supports Container Device Interface (CDI) devices ([#17833](https://github.com/containers/podman/issues/17833)). - The `podman kube play` command now supports Container Device Interface (CDI) devices ([#17833](https://github.com/containers/podman/issues/17833)).
- The `podman machine init` command now supports a new option, `--playbook`, to run an Ansible playbook in the created VM on first boot for initial configuration.
- Quadlet `.pod` files now support a new field, `ShmSize`, to specify the size of the pod's shared SHM ([#22915](https://github.com/containers/podman/issues/22915)). - Quadlet `.pod` files now support a new field, `ShmSize`, to specify the size of the pod's shared SHM ([#22915](https://github.com/containers/podman/issues/22915)).
- The `podman run`, `podman create`, and `podman pod create` commands now support a new option, `--hosts-file`, to define the base file used for `/etc/hosts` in the container. - The `podman run`, `podman create`, and `podman pod create` commands now support a new option, `--hosts-file`, to define the base file used for `/etc/hosts` in the container.
- The `podman run`, `podman create`, and `podman pod create` commands now support a new option, `--no-hostname`, which disables the creation of `/etc/hostname` in the container ([#25002](https://github.com/containers/podman/issues/25002)). - The `podman run`, `podman create`, and `podman pod create` commands now support a new option, `--no-hostname`, which disables the creation of `/etc/hostname` in the container ([#25002](https://github.com/containers/podman/issues/25002)).
@ -23,6 +24,7 @@
### Changes ### Changes
- Podman now passes container hostnames to Netavark, which will use them for any DHCP requests for the container. - Podman now passes container hostnames to Netavark, which will use them for any DHCP requests for the container.
- Partial pulls of `zstd:chunked` images now only happen for images that have a `RootFS.DiffID` entry in the image's OCI config JSON, and require the layer contents to match. This resolves issues with image ID ambiguity when partial pulls were enabled. - Partial pulls of `zstd:chunked` images now only happen for images that have a `RootFS.DiffID` entry in the image's OCI config JSON, and require the layer contents to match. This resolves issues with image ID ambiguity when partial pulls were enabled.
- Packagers can now set the `BUILD_ORIGIN` environment variable when building podman from the `Makefile`. This provides information on who built the Podman binary, and is displayed in `podman version` and `podman info`. This will help upstream bug reports, allowing maintainers to trace how and where the binary was built and installed from.
### Bugfixes ### Bugfixes
- Fixed a bug where `podman machine` VMs on WSL could fail to start when using usermode networking could fail to start due to a port conflict ([#20327](https://github.com/containers/podman/issues/20327)). - Fixed a bug where `podman machine` VMs on WSL could fail to start when using usermode networking could fail to start due to a port conflict ([#20327](https://github.com/containers/podman/issues/20327)).
@ -46,13 +48,20 @@
- Fixed a bug where the output of `podman inspect` on containers did not include the ID of the network the container was joined to, improving Docker compatibility ([#24910](https://github.com/containers/podman/issues/24910)). - Fixed a bug where the output of `podman inspect` on containers did not include the ID of the network the container was joined to, improving Docker compatibility ([#24910](https://github.com/containers/podman/issues/24910)).
- Fixed a bug where containers created with the remote API incorrectly included a create command ([#25026](https://github.com/containers/podman/issues/25026)). - Fixed a bug where containers created with the remote API incorrectly included a create command ([#25026](https://github.com/containers/podman/issues/25026)).
- Fixed a bug where it was possible to specify the `libkrun` backend for VMs on Intel Macs (`libkrun` only supports Arm systems). - Fixed a bug where it was possible to specify the `libkrun` backend for VMs on Intel Macs (`libkrun` only supports Arm systems).
- Fixed a bug where `libkrun` and `applehv` VMs from `podman machine` could be started at the same time on Macs ([#25112](https://github.com/containers/podman/issues/25112)).
- Fixed a bug where `podman exec` commands could not detach from the exec session using the detach keys ([#24895](https://github.com/containers/podman/issues/24895)).
### API ### API
- The Compat and Libpod Build APIs for Images now support a new query parameter, `nohosts`, which (when set to true) does not create `/etc/hosts` in the image when building. - The Compat and Libpod Build APIs for Images now support a new query parameter, `nohosts`, which (when set to true) does not create `/etc/hosts` in the image when building.
- Fixed a bug where the Compat Create API for Containers did not honor CDI devices, preventing (among other things) the use of GPUs with `docker compose` ([#19338](https://github.com/containers/podman/issues/19338)).
### Misc ### Misc
- The Docker alias script has been fixed to better handle variable substitution. - The Docker alias script has been fixed to better handle variable substitution.
- Fixed a bug where `podman-restart.service` functioned incorrectly when no containers were present. - Fixed a bug where `podman-restart.service` functioned incorrectly when no containers were present.
- Updated Buildah to v1.39.0
- Updated the containers/common library to v0.62.0
- Updated the containers/storage library to v1.57.1
- Updated the containers/image library to v5.34.0
## 5.3.2 ## 5.3.2
### Security ### Security

View File

@ -3,15 +3,17 @@ package artifact
import ( import (
"fmt" "fmt"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common" "github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry" "github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities" "github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var (
addCmd = &cobra.Command{ addCmd = &cobra.Command{
Use: "add ARTIFACT PATH [...PATH]", Use: "add [options] ARTIFACT PATH [...PATH]",
Short: "Add an OCI artifact to the local store", Short: "Add an OCI artifact to the local store",
Long: "Add an OCI artifact to the local store from the local filesystem", Long: "Add an OCI artifact to the local store from the local filesystem",
RunE: add, RunE: add,
@ -22,15 +24,41 @@ var (
} }
) )
type artifactAddOptions struct {
ArtifactType string
Annotations []string
}
var (
addOpts artifactAddOptions
)
func init() { func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{ registry.Commands = append(registry.Commands, registry.CliCommand{
Command: addCmd, Command: addCmd,
Parent: artifactCmd, Parent: artifactCmd,
}) })
flags := addCmd.Flags()
annotationFlagName := "annotation"
flags.StringArrayVar(&addOpts.Annotations, annotationFlagName, nil, "set an `annotation` for the specified artifact")
_ = addCmd.RegisterFlagCompletionFunc(annotationFlagName, completion.AutocompleteNone)
addTypeFlagName := "type"
flags.StringVar(&addOpts.ArtifactType, addTypeFlagName, "", "Use type to describe an artifact")
_ = addCmd.RegisterFlagCompletionFunc(addTypeFlagName, completion.AutocompleteNone)
} }
func add(cmd *cobra.Command, args []string) error { func add(cmd *cobra.Command, args []string) error {
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), args[0], args[1:], entities.ArtifactAddoptions{}) opts := new(entities.ArtifactAddOptions)
annots, err := utils.ParseAnnotations(addOpts.Annotations)
if err != nil {
return err
}
opts.Annotations = annots
opts.ArtifactType = addOpts.ArtifactType
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), args[0], args[1:], opts)
if err != nil { if err != nil {
return err return err
} }

View File

@ -32,6 +32,8 @@ var (
type listFlagType struct { type listFlagType struct {
format string format string
noHeading bool
noTrunc bool
} }
type artifactListOutput struct { type artifactListOutput struct {
@ -54,6 +56,8 @@ func init() {
formatFlagName := "format" formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, defaultArtifactListOutputFormat, "Format volume output using JSON or a Go template") flags.StringVar(&listFlag.format, formatFlagName, defaultArtifactListOutputFormat, "Format volume output using JSON or a Go template")
_ = listCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&artifactListOutput{})) _ = listCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&artifactListOutput{}))
flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print column headings")
flags.BoolVar(&listFlag.noTrunc, "no-trunc", false, "Do not truncate output")
} }
func list(cmd *cobra.Command, _ []string) error { func list(cmd *cobra.Command, _ []string) error {
@ -95,10 +99,15 @@ func outputTemplate(cmd *cobra.Command, lrs []*entities.ArtifactListReport) erro
if err != nil { if err != nil {
return err return err
} }
// TODO when we default to shorter ids, i would foresee a switch
// like images that will show the full ids. artifactHash := artifactDigest.Encoded()[0:12]
// If the user does not want truncated hashes
if listFlag.noTrunc {
artifactHash = artifactDigest.Encoded()
}
artifacts = append(artifacts, artifactListOutput{ artifacts = append(artifacts, artifactListOutput{
Digest: artifactDigest.Encoded(), Digest: artifactHash,
Repository: named.Name(), Repository: named.Name(),
Size: units.HumanSize(float64(lr.Artifact.TotalSizeBytes())), Size: units.HumanSize(float64(lr.Artifact.TotalSizeBytes())),
Tag: tag, Tag: tag,
@ -125,7 +134,7 @@ func outputTemplate(cmd *cobra.Command, lrs []*entities.ArtifactListReport) erro
return err return err
} }
if rpt.RenderHeaders { if rpt.RenderHeaders && !listFlag.noHeading {
if err := rpt.Execute(headers); err != nil { if err := rpt.Execute(headers); err != nil {
return fmt.Errorf("failed to write report column headers: %w", err) return fmt.Errorf("failed to write report column headers: %w", err)
} }

View File

@ -6,6 +6,7 @@ type clientInfo struct {
OSArch string `json:"OS"` OSArch string `json:"OS"`
Provider string `json:"provider"` Provider string `json:"provider"`
Version string `json:"version"` Version string `json:"version"`
BuildOrigin string `json:"buildOrigin,omitempty" yaml:",omitempty"`
} }
func getClientInfo() (*clientInfo, error) { func getClientInfo() (*clientInfo, error) {
@ -21,5 +22,6 @@ func getClientInfo() (*clientInfo, error) {
OSArch: vinfo.OsArch, OSArch: vinfo.OsArch,
Provider: p, Provider: p,
Version: vinfo.Version, Version: vinfo.Version,
BuildOrigin: vinfo.BuildOrigin,
}, nil }, nil
} }

View File

@ -3,6 +3,7 @@ package common
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"io/fs"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -1646,8 +1647,8 @@ func AutocompleteContainersConfModules(cmd *cobra.Command, args []string, toComp
for _, d := range dirs { for _, d := range dirs {
cleanedD := filepath.Clean(d) cleanedD := filepath.Clean(d)
moduleD := cleanedD + string(os.PathSeparator) moduleD := cleanedD + string(os.PathSeparator)
_ = filepath.Walk(d, _ = filepath.WalkDir(d,
func(path string, f os.FileInfo, err error) error { func(path string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
return err return err
} }
@ -1657,7 +1658,7 @@ func AutocompleteContainersConfModules(cmd *cobra.Command, args []string, toComp
return nil return nil
} }
if filepath.Clean(path) == cleanedD || f.IsDir() { if filepath.Clean(path) == cleanedD || d.IsDir() {
return nil return nil
} }

View File

@ -62,6 +62,10 @@ func init() {
) )
_ = initCmd.RegisterFlagCompletionFunc(cpusFlagName, completion.AutocompleteNone) _ = initCmd.RegisterFlagCompletionFunc(cpusFlagName, completion.AutocompleteNone)
runPlaybookFlagName := "playbook"
flags.StringVar(&initOpts.PlaybookPath, runPlaybookFlagName, "", "Run an Ansible playbook after first boot")
_ = initCmd.RegisterFlagCompletionFunc(runPlaybookFlagName, completion.AutocompleteDefault)
diskSizeFlagName := "disk-size" diskSizeFlagName := "disk-size"
flags.Uint64Var( flags.Uint64Var(
&initOpts.DiskSize, &initOpts.DiskSize,

View File

@ -45,6 +45,11 @@ var (
debug bool debug bool
) )
type infoReport struct {
define.Info
Client *define.Version `json:",omitempty" yaml:",omitempty"`
}
func init() { func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{ registry.Commands = append(registry.Commands, registry.CliCommand{
Command: infoCommand, Command: infoCommand,
@ -74,12 +79,21 @@ func info(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
remote := registry.IsRemote()
info.Host.ServiceIsRemote = remote
info.Host.ServiceIsRemote = registry.IsRemote() infoReport := infoReport{
Info: *info,
}
if remote {
clientVers, _ := define.GetVersion()
infoReport.Client = &clientVers
}
switch { switch {
case report.IsJSON(inFormat): case report.IsJSON(inFormat):
b, err := json.MarshalIndent(info, "", " ") b, err := json.MarshalIndent(infoReport, "", " ")
if err != nil { if err != nil {
return err return err
} }
@ -94,9 +108,9 @@ func info(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
return rpt.Execute(info) return rpt.Execute(infoReport)
default: default:
b, err := yaml.Marshal(info) b, err := yaml.Marshal(infoReport)
if err != nil { if err != nil {
return err return err
} }

View File

@ -97,6 +97,7 @@ API Version:\t{{.APIVersion}}
Go Version:\t{{.GoVersion}} Go Version:\t{{.GoVersion}}
{{if .GitCommit -}}Git Commit:\t{{.GitCommit}}\n{{end -}} {{if .GitCommit -}}Git Commit:\t{{.GitCommit}}\n{{end -}}
Built:\t{{.BuiltTime}} Built:\t{{.BuiltTime}}
{{if .BuildOrigin -}}Build Origin:\t{{.BuildOrigin}}\n{{end -}}
OS/Arch:\t{{.OsArch}} OS/Arch:\t{{.OsArch}}
{{- end}} {{- end}}
@ -108,6 +109,7 @@ API Version:\t{{.APIVersion}}
Go Version:\t{{.GoVersion}} Go Version:\t{{.GoVersion}}
{{if .GitCommit -}}Git Commit:\t{{.GitCommit}}\n{{end -}} {{if .GitCommit -}}Git Commit:\t{{.GitCommit}}\n{{end -}}
Built:\t{{.BuiltTime}} Built:\t{{.BuiltTime}}
{{if .BuildOrigin -}}Build Origin:\t{{.BuildOrigin}}\n{{end -}}
OS/Arch:\t{{.OsArch}} OS/Arch:\t{{.OsArch}}
{{- end}}{{- end}} {{- end}}{{- end}}
` `

View File

@ -9,6 +9,7 @@ PRODUCTSIGN_IDENTITY=${PRODUCTSIGN_IDENTITY:-mock}
NO_CODESIGN=${NO_CODESIGN:-0} NO_CODESIGN=${NO_CODESIGN:-0}
HELPER_BINARIES_DIR="/opt/podman/bin" HELPER_BINARIES_DIR="/opt/podman/bin"
MACHINE_POLICY_JSON_DIR="/opt/podman/config" MACHINE_POLICY_JSON_DIR="/opt/podman/config"
BUILD_ORIGIN="pkginstaller"
tmpBin="contrib/pkginstaller/tmp-bin" tmpBin="contrib/pkginstaller/tmp-bin"
@ -47,7 +48,7 @@ function build_podman() {
} }
function build_podman_arch(){ function build_podman_arch(){
make -B GOARCH="$1" podman-remote HELPER_BINARIES_DIR="${HELPER_BINARIES_DIR}" make -B GOARCH="$1" podman-remote HELPER_BINARIES_DIR="${HELPER_BINARIES_DIR}" BUILD_ORIGIN="${BUILD_ORIGIN}"
make -B GOARCH="$1" podman-mac-helper make -B GOARCH="$1" podman-mac-helper
mkdir -p "${tmpBin}" mkdir -p "${tmpBin}"
cp bin/darwin/podman "${tmpBin}/podman-$1" cp bin/darwin/podman "${tmpBin}/podman-$1"

View File

@ -1,3 +1,5 @@
podman-artifact-add.1.md
podman-artifact-ls.1.md
podman-artifact-pull.1.md podman-artifact-pull.1.md
podman-artifact-push.1.md podman-artifact-push.1.md
podman-attach.1.md podman-attach.1.md

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman manifest add, manifest annotate ####> podman artifact add, manifest add, manifest annotate
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--annotation**=*annotation=value* #### **--annotation**=*annotation=value*

View File

@ -0,0 +1,7 @@
####> This option file is used in:
####> podman artifact ls, images
####> If file is edited, make sure the changes
####> are applicable to all of those.
#### **--no-trunc**
Do not truncate the output (default *false*).

View File

@ -1,5 +1,5 @@
####> This option file is used in: ####> This option file is used in:
####> podman image trust, images, machine list, network ls, pod ps, secret ls, volume ls ####> podman artifact ls, image trust, images, machine list, network ls, pod ps, secret ls, volume ls
####> If file is edited, make sure the changes ####> If file is edited, make sure the changes
####> are applicable to all of those. ####> are applicable to all of those.
#### **--noheading**, **-n** #### **--noheading**, **-n**

View File

@ -19,10 +19,15 @@ added.
## OPTIONS ## OPTIONS
@@option annotation.manifest
#### **--help** #### **--help**
Print usage statement. Print usage statement.
#### **--type**
Set a type for the artifact being added.
## EXAMPLES ## EXAMPLES
@ -39,6 +44,10 @@ $ podman artifact add quay.io/myartifact/myml:latest /tmp/foobar1.ml /tmp/foobar
1487acae11b5a30948c50762882036b41ac91a7b9514be8012d98015c95ddb78 1487acae11b5a30948c50762882036b41ac91a7b9514be8012d98015c95ddb78
``` ```
Set an annotation for an artifact
```
$ podman artifact add --annotation date=2025-01-30 quay.io/myartifact/myml:latest /tmp/foobar1.ml
```
## SEE ALSO ## SEE ALSO

View File

@ -28,7 +28,9 @@ Print results with a Go template.
| .Size | Size artifact in human readable units | | .Size | Size artifact in human readable units |
| .Tag | Tag of the artifact name | | .Tag | Tag of the artifact name |
@@option no-trunc
@@option noheading
## EXAMPLES ## EXAMPLES
@ -36,16 +38,30 @@ List artifacts in the local store
``` ```
$ podman artifact ls $ podman artifact ls
REPOSITORY TAG DIGEST SIZE REPOSITORY TAG DIGEST SIZE
quay.io/artifact/foobar1 latest ab609fad386d 2.097GB
quay.io/artifact/foobar2 special cd734b558ceb 12.58MB
```
List artifacts in the local store without truncating the digest
```
$ podman artifact ls --no-trunc
REPOSITORY TAG DIGEST SIZE
quay.io/artifact/foobar1 latest ab609fad386df1433f461b0643d9cf575560baf633809dcc9c190da6cc3a3c29 2.097GB quay.io/artifact/foobar1 latest ab609fad386df1433f461b0643d9cf575560baf633809dcc9c190da6cc3a3c29 2.097GB
quay.io/artifact/foobar2 special cd734b558ceb8ccc0281ca76530e1dea1eb479407d3163f75fb601bffb6f73d0 12.58MB quay.io/artifact/foobar2 special cd734b558ceb8ccc0281ca76530e1dea1eb479407d3163f75fb601bffb6f73d0 12.58MB
``` ```
List artifacts in the local store without the title header
```
$ podman artifact ls --noheading
quay.io/artifact/foobar1 latest ab609fad386d 2.097GB
quay.io/artifact/foobar2 special cd734b558ceb 12.58MB
```
List artifact digests and size using a --format List artifact digests and size using a --format
``` ```
$ podman artifact ls --format "{{.Digest}} {{.Size}}" $ podman artifact ls --format "{{.Digest}} {{.Size}}"
ab609fad386df1433f461b0643d9cf575560baf633809dcc9c190da6cc3a3c29 2.097GB ab609fad386d 2.097GB
cd734b558ceb8ccc0281ca76530e1dea1eb479407d3163f75fb601bffb6f73d0 12.58MB cd734b558ceb 12.58MB
``` ```

View File

@ -106,9 +106,7 @@ Valid placeholders for the Go template are listed below:
Display the history of image names. If an image gets re-tagged or untagged, then the image name history gets prepended (latest image first). This is especially useful when undoing a tag operation or an image does not contain any name because it has been untagged. Display the history of image names. If an image gets re-tagged or untagged, then the image name history gets prepended (latest image first). This is especially useful when undoing a tag operation or an image does not contain any name because it has been untagged.
#### **--no-trunc** @@option no-trunc
Do not truncate the output (default *false*).
@@option noheading @@option noheading

View File

@ -80,6 +80,8 @@ is copied into the user's CONF_DIR and renamed. Additionally, no SSH keys are g
Fully qualified registry, path, or URL to a VM image. Fully qualified registry, path, or URL to a VM image.
Registry target must be in the form of `docker://registry/repo/image:version`. Registry target must be in the form of `docker://registry/repo/image:version`.
Note: Only images provided by podman will be supported.
#### **--memory**, **-m**=*number* #### **--memory**, **-m**=*number*
Memory (in MiB). Note: 1024MiB = 1GiB. Memory (in MiB). Note: 1024MiB = 1GiB.
@ -88,6 +90,13 @@ Memory (in MiB). Note: 1024MiB = 1GiB.
Start the virtual machine immediately after it has been initialized. Start the virtual machine immediately after it has been initialized.
#### **--playbook**
Add the provided Ansible playbook to the machine and execute it after the first boot.
Note: The playbook will be executed with the same privileges given to the user in the virtual machine. The playbook provided cannot include other files from the host system, as they will not be copied.
Use of the `--playbook` flag will require the image to include Ansible. The default image provided will have Ansible included.
#### **--rootful** #### **--rootful**
Whether this machine prefers rootful (`true`) or rootless (`false`) Whether this machine prefers rootful (`true`) or rootless (`false`)

View File

@ -16,6 +16,9 @@ var (
// BuildInfo is the time at which the binary was built // BuildInfo is the time at which the binary was built
// It will be populated by the Makefile. // It will be populated by the Makefile.
buildInfo string buildInfo string
// BuildOrigin is the packager of the binary.
// It will be populated at build-time.
buildOrigin string
) )
// Version is an output struct for API // Version is an output struct for API
@ -26,6 +29,7 @@ type Version struct {
GitCommit string GitCommit string
BuiltTime string BuiltTime string
Built int64 Built int64
BuildOrigin string `json:",omitempty" yaml:",omitempty"`
OsArch string OsArch string
Os string Os string
} }
@ -49,6 +53,7 @@ func GetVersion() (Version, error) {
GitCommit: gitCommit, GitCommit: gitCommit,
BuiltTime: time.Unix(buildTime, 0).Format(time.ANSIC), BuiltTime: time.Unix(buildTime, 0).Format(time.ANSIC),
Built: buildTime, Built: buildTime,
BuildOrigin: buildOrigin,
OsArch: runtime.GOOS + "/" + runtime.GOARCH, OsArch: runtime.GOOS + "/" + runtime.GOARCH,
Os: runtime.GOOS, Os: runtime.GOOS,
}, nil }, nil

View File

@ -163,6 +163,11 @@ func cliOpts(cc handlers.CreateContainerConfig, rtc *config.Config) (*entities.C
for _, dev := range cc.HostConfig.Devices { for _, dev := range cc.HostConfig.Devices {
devices = append(devices, fmt.Sprintf("%s:%s:%s", dev.PathOnHost, dev.PathInContainer, dev.CgroupPermissions)) devices = append(devices, fmt.Sprintf("%s:%s:%s", dev.PathOnHost, dev.PathInContainer, dev.CgroupPermissions))
} }
for _, r := range cc.HostConfig.Resources.DeviceRequests {
if r.Driver == "cdi" {
devices = append(devices, r.DeviceIDs...)
}
}
// iterate blkreaddevicebps // iterate blkreaddevicebps
readBps := make([]string, 0, len(cc.HostConfig.BlkioDeviceReadBps)) readBps := make([]string, 0, len(cc.HostConfig.BlkioDeviceReadBps))

View File

@ -29,6 +29,7 @@ import (
"github.com/containers/podman/v5/pkg/channel" "github.com/containers/podman/v5/pkg/channel"
"github.com/containers/podman/v5/pkg/domain/entities" "github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/infra/abi" "github.com/containers/podman/v5/pkg/domain/infra/abi"
domainUtils "github.com/containers/podman/v5/pkg/domain/utils"
"github.com/containers/podman/v5/pkg/errorhandling" "github.com/containers/podman/v5/pkg/errorhandling"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/schema" "github.com/gorilla/schema"
@ -520,24 +521,17 @@ func ManifestModify(w http.ResponseWriter, r *http.Request) {
return return
} }
annotationsFromAnnotationSlice := func(annotation []string) map[string]string {
annotations := make(map[string]string)
for _, annotationSpec := range annotation {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("no value given for annotation %q", key))
return nil
}
annotations[key] = val
}
return annotations
}
if len(body.ManifestAddOptions.Annotation) != 0 { if len(body.ManifestAddOptions.Annotation) != 0 {
if len(body.ManifestAddOptions.Annotations) != 0 { if len(body.ManifestAddOptions.Annotations) != 0 {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both Annotation and Annotations")) utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both Annotation and Annotations"))
return return
} }
body.ManifestAddOptions.Annotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.Annotation) annots, err := domainUtils.ParseAnnotations(body.ManifestAddOptions.Annotation)
if err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
body.ManifestAddOptions.Annotations = annots
body.ManifestAddOptions.Annotation = nil body.ManifestAddOptions.Annotation = nil
} }
if len(body.ManifestAddOptions.IndexAnnotation) != 0 { if len(body.ManifestAddOptions.IndexAnnotation) != 0 {
@ -545,7 +539,12 @@ func ManifestModify(w http.ResponseWriter, r *http.Request) {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both IndexAnnotation and IndexAnnotations")) utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both IndexAnnotation and IndexAnnotations"))
return return
} }
body.ManifestAddOptions.IndexAnnotations = annotationsFromAnnotationSlice(body.ManifestAddOptions.IndexAnnotation) annots, err := domainUtils.ParseAnnotations(body.ManifestAddOptions.IndexAnnotation)
if err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
body.ManifestAddOptions.IndexAnnotations = annots
body.ManifestAddOptions.IndexAnnotation = nil body.ManifestAddOptions.IndexAnnotation = nil
} }

View File

@ -9,7 +9,8 @@ import (
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
) )
type ArtifactAddoptions struct { type ArtifactAddOptions struct {
Annotations map[string]string
ArtifactType string ArtifactType string
} }

View File

@ -9,7 +9,7 @@ import (
) )
type ImageEngine interface { //nolint:interfacebloat type ImageEngine interface { //nolint:interfacebloat
ArtifactAdd(ctx context.Context, name string, paths []string, opts ArtifactAddoptions) (*ArtifactAddReport, error) ArtifactAdd(ctx context.Context, name string, paths []string, opts *ArtifactAddOptions) (*ArtifactAddReport, error)
ArtifactInspect(ctx context.Context, name string, opts ArtifactInspectOptions) (*ArtifactInspectReport, error) ArtifactInspect(ctx context.Context, name string, opts ArtifactInspectOptions) (*ArtifactInspectReport, error)
ArtifactList(ctx context.Context, opts ArtifactListOptions) ([]*ArtifactListReport, error) ArtifactList(ctx context.Context, opts ArtifactListOptions) ([]*ArtifactListReport, error)
ArtifactPull(ctx context.Context, name string, opts ArtifactPullOptions) (*ArtifactPullReport, error) ArtifactPull(ctx context.Context, name string, opts ArtifactPullOptions) (*ArtifactPullReport, error)

View File

@ -11,6 +11,7 @@ import (
"github.com/containers/common/libimage" "github.com/containers/common/libimage"
"github.com/containers/podman/v5/pkg/domain/entities" "github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/libartifact/store" "github.com/containers/podman/v5/pkg/libartifact/store"
"github.com/containers/podman/v5/pkg/libartifact/types"
) )
func getDefaultArtifactStore(ir *ImageEngine) string { func getDefaultArtifactStore(ir *ImageEngine) string {
@ -152,12 +153,18 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit
err = artStore.Push(ctx, name, name, copyOpts) err = artStore.Push(ctx, name, name, copyOpts)
return &entities.ArtifactPushReport{}, err return &entities.ArtifactPushReport{}, err
} }
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts entities.ArtifactAddoptions) (*entities.ArtifactAddReport, error) { func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext()) artStore, err := store.NewArtifactStore(getDefaultArtifactStore(ir), ir.Libpod.SystemContext())
if err != nil { if err != nil {
return nil, err return nil, err
} }
artifactDigest, err := artStore.Add(ctx, name, paths, opts.ArtifactType)
addOptions := types.AddOptions{
Annotations: opts.Annotations,
ArtifactType: opts.ArtifactType,
}
artifactDigest, err := artStore.Add(ctx, name, paths, &addOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -9,7 +9,7 @@ import (
// TODO For now, no remote support has been added. We need the API to firm up first. // TODO For now, no remote support has been added. We need the API to firm up first.
func ArtifactAdd(ctx context.Context, path, name string, opts entities.ArtifactAddoptions) error { func ArtifactAdd(ctx context.Context, path, name string, opts entities.ArtifactAddOptions) error {
return fmt.Errorf("not implemented") return fmt.Errorf("not implemented")
} }
@ -33,6 +33,6 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit
return nil, fmt.Errorf("not implemented") return nil, fmt.Errorf("not implemented")
} }
func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts entities.ArtifactAddoptions) (*entities.ArtifactAddReport, error) { func (ir *ImageEngine) ArtifactAdd(ctx context.Context, name string, paths []string, opts *entities.ArtifactAddOptions) (*entities.ArtifactAddReport, error) {
return nil, fmt.Errorf("not implemented") return nil, fmt.Errorf("not implemented")
} }

View File

@ -1,6 +1,7 @@
package utils package utils
import ( import (
"fmt"
"net/url" "net/url"
"strings" "strings"
@ -39,3 +40,17 @@ func ToURLValues(f []string) (filters url.Values) {
} }
return return
} }
// ParseAnnotations takes a string slice of options, expected to be "key=val" and returns
// a string map where the map index is the key and points to the value
func ParseAnnotations(options []string) (map[string]string, error) {
annotations := make(map[string]string)
for _, annotationSpec := range options {
key, val, hasVal := strings.Cut(annotationSpec, "=")
if !hasVal {
return nil, fmt.Errorf("no value given for annotation %q", key)
}
annotations[key] = val
}
return annotations, nil
}

View File

@ -11,18 +11,18 @@ import (
) )
type Artifact struct { type Artifact struct {
Manifests []manifest.OCI1 // Manifest is the OCI manifest for the artifact with the name.
// In a valid artifact the Manifest is guaranteed to not be nil.
Manifest *manifest.OCI1
Name string Name string
} }
// TotalSizeBytes returns the total bytes of the all the artifact layers // TotalSizeBytes returns the total bytes of the all the artifact layers
func (a *Artifact) TotalSizeBytes() int64 { func (a *Artifact) TotalSizeBytes() int64 {
var s int64 var s int64
for _, artifact := range a.Manifests { for _, layer := range a.Manifest.Layers {
for _, layer := range artifact.Layers {
s += layer.Size s += layer.Size
} }
}
return s return s
} }
@ -45,13 +45,7 @@ func (a *Artifact) SetName(name string) {
} }
func (a *Artifact) GetDigest() (*digest.Digest, error) { func (a *Artifact) GetDigest() (*digest.Digest, error) {
if len(a.Manifests) > 1 { b, err := json.Marshal(a.Manifest)
return nil, fmt.Errorf("not supported: multiple manifests found in artifact")
}
if len(a.Manifests) < 1 {
return nil, fmt.Errorf("not supported: no manifests found in artifact")
}
b, err := json.Marshal(a.Manifests[0])
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -72,9 +66,6 @@ func (al ArtifactList) GetByNameOrDigest(nameOrDigest string) (*Artifact, bool,
} }
// Before giving up, check by digest // Before giving up, check by digest
for _, artifact := range al { for _, artifact := range al {
// TODO Here we have to assume only a single manifest for the artifact; this will
// need to evolve
if len(artifact.Manifests) > 0 {
artifactDigest, err := artifact.GetDigest() artifactDigest, err := artifact.GetDigest()
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@ -84,6 +75,5 @@ func (al ArtifactList) GetByNameOrDigest(nameOrDigest string) (*Artifact, bool,
return artifact, true, nil return artifact, true, nil
} }
} }
}
return nil, false, fmt.Errorf("no artifact found with name or digest of %s", nameOrDigest) return nil, false, fmt.Errorf("no artifact found with name or digest of %s", nameOrDigest)
} }

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"maps"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -160,7 +161,8 @@ func (as ArtifactStore) Push(ctx context.Context, src, dest string, opts libimag
// Add takes one or more local files and adds them to the local artifact store. The empty // Add takes one or more local files and adds them to the local artifact store. The empty
// string input is for possible custom artifact types. // string input is for possible custom artifact types.
func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _ string) (*digest.Digest, error) { func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, options *libartTypes.AddOptions) (*digest.Digest, error) {
annots := maps.Clone(options.Annotations)
if len(dest) == 0 { if len(dest) == 0 {
return nil, ErrEmptyArtifactName return nil, ErrEmptyArtifactName
} }
@ -191,6 +193,13 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _
defer imageDest.Close() defer imageDest.Close()
for _, path := range paths { for _, path := range paths {
// currently we don't allow override of the filename ; if a user requirement emerges,
// we could seemingly accommodate but broadens possibilities of something bad happening
// for things like `artifact extract`
if _, hasTitle := options.Annotations[specV1.AnnotationTitle]; hasTitle {
return nil, fmt.Errorf("cannot override filename with %s annotation", specV1.AnnotationTitle)
}
// get the new artifact into the local store // get the new artifact into the local store
newBlobDigest, newBlobSize, err := layout.PutBlobFromLocalFile(ctx, imageDest, path) newBlobDigest, newBlobSize, err := layout.PutBlobFromLocalFile(ctx, imageDest, path)
if err != nil { if err != nil {
@ -200,14 +209,16 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _
if err != nil { if err != nil {
return nil, err return nil, err
} }
newArtifactAnnotations := map[string]string{}
newArtifactAnnotations[specV1.AnnotationTitle] = filepath.Base(path) annots[specV1.AnnotationTitle] = filepath.Base(path)
newLayer := specV1.Descriptor{ newLayer := specV1.Descriptor{
MediaType: detectedType, MediaType: detectedType,
Digest: newBlobDigest, Digest: newBlobDigest,
Size: newBlobSize, Size: newBlobSize,
Annotations: newArtifactAnnotations, Annotations: annots,
} }
artifactManifestLayers = append(artifactManifestLayers, newLayer) artifactManifestLayers = append(artifactManifestLayers, newLayer)
} }
@ -215,11 +226,12 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _
Versioned: specs.Versioned{SchemaVersion: 2}, Versioned: specs.Versioned{SchemaVersion: 2},
MediaType: specV1.MediaTypeImageManifest, MediaType: specV1.MediaTypeImageManifest,
// TODO This should probably be configurable once the CLI is capable // TODO This should probably be configurable once the CLI is capable
ArtifactType: "",
Config: specV1.DescriptorEmptyJSON, Config: specV1.DescriptorEmptyJSON,
Layers: artifactManifestLayers, Layers: artifactManifestLayers,
} }
artifactManifest.ArtifactType = options.ArtifactType
rawData, err := json.Marshal(artifactManifest) rawData, err := json.Marshal(artifactManifest)
if err != nil { if err != nil {
return nil, err return nil, err
@ -287,13 +299,13 @@ func (as ArtifactStore) getArtifacts(ctx context.Context, _ *libartTypes.GetArti
if err != nil { if err != nil {
return nil, err return nil, err
} }
manifests, err := getManifests(ctx, imgSrc, nil) manifest, err := getManifest(ctx, imgSrc)
imgSrc.Close() imgSrc.Close()
if err != nil { if err != nil {
return nil, err return nil, err
} }
artifact := libartifact.Artifact{ artifact := libartifact.Artifact{
Manifests: manifests, Manifest: manifest,
} }
if val, ok := l.ManifestDescriptor.Annotations[specV1.AnnotationRefName]; ok { if val, ok := l.ManifestDescriptor.Annotations[specV1.AnnotationRefName]; ok {
artifact.SetName(val) artifact.SetName(val)
@ -304,41 +316,25 @@ func (as ArtifactStore) getArtifacts(ctx context.Context, _ *libartTypes.GetArti
return al, nil return al, nil
} }
// getManifests takes an imgSrc and starting digest (nil means "top") and collects all the manifests "under" // getManifest takes an imgSrc and returns the manifest for the imgSrc.
// it. this func calls itself recursively with a new startingDigest assuming that we are dealing with // A OCI index list is not supported and will return an error.
// an index list func getManifest(ctx context.Context, imgSrc types.ImageSource) (*manifest.OCI1, error) {
func getManifests(ctx context.Context, imgSrc types.ImageSource, startingDigest *digest.Digest) ([]manifest.OCI1, error) { b, manifestType, err := imgSrc.GetManifest(ctx, nil)
var (
manifests []manifest.OCI1
)
b, manifestType, err := imgSrc.GetManifest(ctx, startingDigest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// this assumes that there are only single, and multi-images // We only support a single flat manifest and not an oci index list
if !manifest.MIMETypeIsMultiImage(manifestType) { if manifest.MIMETypeIsMultiImage(manifestType) {
// these are the keepers return nil, fmt.Errorf("manifest %q is index list", imgSrc.Reference().StringWithinTransport())
}
// parse the single manifest
mani, err := manifest.OCI1FromManifest(b) mani, err := manifest.OCI1FromManifest(b)
if err != nil { if err != nil {
return nil, err return nil, err
} }
manifests = append(manifests, *mani) return mani, nil
return manifests, nil
}
// We are dealing with an oci index list
maniList, err := manifest.OCI1IndexFromManifest(b)
if err != nil {
return nil, err
}
for _, m := range maniList.Manifests {
iterManifests, err := getManifests(ctx, imgSrc, &m.Digest)
if err != nil {
return nil, err
}
manifests = append(manifests, iterManifests...)
}
return manifests, nil
} }
func createEmptyStanza(path string) error { func createEmptyStanza(path string) error {

View File

@ -3,3 +3,9 @@ package types
// GetArtifactOptions is a struct containing options that for obtaining artifacts. // GetArtifactOptions is a struct containing options that for obtaining artifacts.
// It is meant for future growth or changes required without wacking the API // It is meant for future growth or changes required without wacking the API
type GetArtifactOptions struct{} type GetArtifactOptions struct{}
// AddOptions are additional descriptors of an artifact file
type AddOptions struct {
Annotations map[string]string `json:"annotations,omitempty"`
ArtifactType string `json:",omitempty"`
}

View File

@ -3,6 +3,7 @@ package define
import "net/url" import "net/url"
type InitOptions struct { type InitOptions struct {
PlaybookPath string
CPUS uint64 CPUS uint64
DiskSize uint64 DiskSize uint64
IgnitionPath string IgnitionPath string

View File

@ -19,11 +19,13 @@ type initMachine struct {
-m, --memory uint Memory in MiB (default 2048) -m, --memory uint Memory in MiB (default 2048)
--now Start machine now --now Start machine now
--rootful Whether this machine should prefer rootful container execution --rootful Whether this machine should prefer rootful container execution
--playbook string Run an ansible playbook after first boot
--timezone string Set timezone (default "local") --timezone string Set timezone (default "local")
-v, --volume stringArray Volumes to mount, source:target -v, --volume stringArray Volumes to mount, source:target
--volume-driver string Optional volume driver --volume-driver string Optional volume driver
*/ */
playbook string
cpus *uint cpus *uint
diskSize *uint diskSize *uint
ignitionPath string ignitionPath string
@ -73,6 +75,9 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string {
if i.rootful { if i.rootful {
cmd = append(cmd, "--rootful") cmd = append(cmd, "--rootful")
} }
if l := len(i.playbook); l > 0 {
cmd = append(cmd, "--playbook", i.playbook)
}
if i.userModeNetworking { if i.userModeNetworking {
cmd = append(cmd, "--user-mode-networking") cmd = append(cmd, "--user-mode-networking")
} }
@ -152,6 +157,11 @@ func (i *initMachine) withRootful(r bool) *initMachine {
return i return i
} }
func (i *initMachine) withRunPlaybook(p string) *initMachine {
i.playbook = p
return i
}
func (i *initMachine) withUserModeNetworking(r bool) *initMachine { //nolint:unused func (i *initMachine) withUserModeNetworking(r bool) *initMachine { //nolint:unused
i.userModeNetworking = r i.userModeNetworking = r
return i return i

View File

@ -98,6 +98,73 @@ var _ = Describe("podman machine init", func() {
} }
}) })
It("run playbook", func() {
str := randomString()
// ansible playbook file to create a text file containing a random string
playbookContents := fmt.Sprintf(`- name: Simple podman machine example
hosts: localhost
tasks:
- name: create a file
ansible.builtin.copy:
dest: ~/foobar.txt
content: "%s\n"`, str)
playbookPath := filepath.Join(GinkgoT().TempDir(), "playbook.yaml")
// create the playbook file
playbookFile, err := os.Create(playbookPath)
Expect(err).ToNot(HaveOccurred())
defer playbookFile.Close()
// write the desired contents into the file
_, err = playbookFile.WriteString(playbookContents)
Expect(err).To(Not(HaveOccurred()))
name := randomString()
i := new(initMachine)
session, err := mb.setName(name).setCmd(i.withImage(mb.imagePath).withRunPlaybook(playbookPath).withNow()).run()
Expect(err).ToNot(HaveOccurred())
Expect(session).To(Exit(0))
// ensure the contents of the playbook file didn't change when getting copied
ssh := new(sshMachine)
sshSession, err := mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", "playbook.yaml"})).run()
Expect(err).ToNot(HaveOccurred())
Expect(sshSession).To(Exit(0))
Expect(sshSession.outputToStringSlice()).To(Equal(strings.Split(playbookContents, "\n")))
// wait until the playbook.service is done before checking to make sure the playbook was a success
playbookFinished := false
for range 900 {
sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"systemctl", "is-active", "playbook.service"})).run()
Expect(err).ToNot(HaveOccurred())
if sshSession.outputToString() == "inactive" {
playbookFinished = true
break
}
time.Sleep(10 * time.Millisecond)
}
if !playbookFinished {
Fail("playbook.service did not finish")
}
// output the contents of the file generated by the playbook
guestUser := "core"
if isWSL() {
guestUser = "user"
}
sshSession, err = mb.setName(name).setCmd(ssh.withSSHCommand([]string{"cat", fmt.Sprintf("/home/%s/foobar.txt", guestUser)})).run()
Expect(err).ToNot(HaveOccurred())
Expect(sshSession).To(Exit(0))
// check its the same as the random number or string that we generated
Expect(sshSession.outputToString()).To(Equal(str))
})
It("simple init with start", func() { It("simple init with start", func() {
i := initMachine{} i := initMachine{}
session, err := mb.setCmd(i.withImage(mb.imagePath)).run() session, err := mb.setCmd(i.withImage(mb.imagePath)).run()

View File

@ -685,6 +685,51 @@ done
` `
} }
func (i *IgnitionBuilder) AddPlaybook(contents string, destPath string, username string) error {
// create the ignition file object
f := File{
Node: Node{
Group: GetNodeGrp(username),
Path: destPath,
User: GetNodeUsr(username),
},
FileEmbedded1: FileEmbedded1{
Append: nil,
Contents: Resource{
Source: EncodeDataURLPtr(contents),
},
Mode: IntToPtr(0744),
},
}
// call ignitionBuilder.WithFile
// add the config file to the ignition object
i.WithFile(f)
unit := parser.NewUnitFile()
unit.Add("Unit", "After", "ready.service")
unit.Add("Unit", "ConditionFirstBoot", "yes")
unit.Add("Service", "Type", "oneshot")
unit.Add("Service", "User", username)
unit.Add("Service", "Group", username)
unit.Add("Service", "ExecStart", fmt.Sprintf("ansible-playbook %s", destPath))
unit.Add("Install", "WantedBy", "default.target")
unitContents, err := unit.ToString()
if err != nil {
return err
}
// create a systemd service
playbookUnit := Unit{
Enabled: BoolToPtr(true),
Name: "playbook.service",
Contents: &unitContents,
}
i.WithUnit(playbookUnit)
return nil
}
func GetNetRecoveryUnitFile() *parser.UnitFile { func GetNetRecoveryUnitFile() *parser.UnitFile {
recoveryUnit := parser.NewUnitFile() recoveryUnit := parser.NewUnitFile()
recoveryUnit.Add("Unit", "Description", "Verifies health of network and recovers if necessary") recoveryUnit.Add("Unit", "Description", "Verifies health of network and recovers if necessary")

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"io"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -207,6 +208,32 @@ func Init(opts machineDefine.InitOptions, mp vmconfigs.VMProvider) error {
} }
} }
if len(opts.PlaybookPath) > 0 {
f, err := os.Open(opts.PlaybookPath)
if err != nil {
return err
}
s, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("read playbook: %w", err)
}
playbookDest := fmt.Sprintf("/home/%s/%s", userName, "playbook.yaml")
if mp.VMType() != machineDefine.WSLVirt {
err = ignBuilder.AddPlaybook(string(s), playbookDest, userName)
if err != nil {
return err
}
}
mc.Ansible = &vmconfigs.AnsibleConfig{
PlaybookPath: playbookDest,
Contents: string(s),
User: userName,
}
}
readyIgnOpts, err := mp.PrepareIgnition(mc, &ignBuilder) readyIgnOpts, err := mp.PrepareIgnition(mc, &ignBuilder)
if err != nil { if err != nil {
return err return err
@ -543,6 +570,16 @@ func Start(mc *vmconfigs.MachineConfig, mp vmconfigs.VMProvider, dirs *machineDe
} }
} }
isFirstBoot, err := mc.IsFirstBoot()
if err != nil {
logrus.Error(err)
}
if mp.VMType() == machineDefine.WSLVirt && mc.Ansible != nil && isFirstBoot {
if err := machine.CommonSSHSilent(mc.Ansible.User, mc.SSH.IdentityPath, mc.Name, mc.SSH.Port, []string{"ansible-playbook", mc.Ansible.PlaybookPath}); err != nil {
logrus.Error(err)
}
}
// Provider is responsible for waiting // Provider is responsible for waiting
if mp.UseProviderNetworkSetup() { if mp.UseProviderNetworkSetup() {
return nil return nil

View File

@ -53,6 +53,8 @@ type MachineConfig struct {
Starting bool Starting bool
Rosetta bool Rosetta bool
Ansible *AnsibleConfig
} }
type machineImage interface { //nolint:unused type machineImage interface { //nolint:unused
@ -148,3 +150,9 @@ type VMStats struct {
// LastUp contains the last recorded uptime // LastUp contains the last recorded uptime
LastUp time.Time LastUp time.Time
} }
type AnsibleConfig struct {
PlaybookPath string
Contents string
User string
}

View File

@ -148,7 +148,7 @@ func createKeys(mc *vmconfigs.MachineConfig, dist string) error {
return nil return nil
} }
func configureSystem(mc *vmconfigs.MachineConfig, dist string) error { func configureSystem(mc *vmconfigs.MachineConfig, dist string, ansibleConfig *vmconfigs.AnsibleConfig) error {
user := mc.SSH.RemoteUsername user := mc.SSH.RemoteUsername
if err := wslInvoke(dist, "sh", "-c", fmt.Sprintf(appendPort, mc.SSH.Port, mc.SSH.Port)); err != nil { if err := wslInvoke(dist, "sh", "-c", fmt.Sprintf(appendPort, mc.SSH.Port, mc.SSH.Port)); err != nil {
return fmt.Errorf("could not configure SSH port for guest OS: %w", err) return fmt.Errorf("could not configure SSH port for guest OS: %w", err)
@ -167,6 +167,12 @@ func configureSystem(mc *vmconfigs.MachineConfig, dist string) error {
return fmt.Errorf("could not generate systemd-sysusers override for guest OS: %w", err) return fmt.Errorf("could not generate systemd-sysusers override for guest OS: %w", err)
} }
if ansibleConfig != nil {
if err := wslPipe(ansibleConfig.Contents, dist, "sh", "-c", fmt.Sprintf("cat > %s", ansibleConfig.PlaybookPath)); err != nil {
return fmt.Errorf("could not generate playbook file for guest os: %w", err)
}
}
lingerCmd := withUser("cat > /home/[USER]/.config/systemd/[USER]/linger-example.service", user) lingerCmd := withUser("cat > /home/[USER]/.config/systemd/[USER]/linger-example.service", user)
if err := wslPipe(lingerService, dist, "sh", "-c", lingerCmd); err != nil { if err := wslPipe(lingerService, dist, "sh", "-c", lingerCmd); err != nil {
return fmt.Errorf("could not generate linger service for guest OS: %w", err) return fmt.Errorf("could not generate linger service for guest OS: %w", err)

View File

@ -68,7 +68,7 @@ func (w WSLStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConf
} }
fmt.Println("Configuring system...") fmt.Println("Configuring system...")
if err = configureSystem(mc, dist); err != nil { if err = configureSystem(mc, dist, mc.Ansible); err != nil {
return err return err
} }

View File

@ -364,7 +364,6 @@ get_cmd_line_args (int *argc_out)
static bool static bool
can_use_shortcut (char **argv) can_use_shortcut (char **argv)
{ {
cleanup_free char *argv0 = NULL;
bool ret = true; bool ret = true;
int argc; int argc;
@ -372,8 +371,6 @@ can_use_shortcut (char **argv)
return false; return false;
#endif #endif
argv0 = argv[0];
if (strstr (argv[0], "podman") == NULL) if (strstr (argv[0], "podman") == NULL)
return false; return false;
@ -439,6 +436,7 @@ static void __attribute__((constructor)) init()
const char *listen_fds; const char *listen_fds;
const char *listen_fdnames; const char *listen_fdnames;
cleanup_free char **argv = NULL; cleanup_free char **argv = NULL;
cleanup_free char *argv0 = NULL;
cleanup_dir DIR *d = NULL; cleanup_dir DIR *d = NULL;
int argc; int argc;
@ -496,6 +494,8 @@ static void __attribute__((constructor)) init()
fprintf(stderr, "cannot retrieve cmd line"); fprintf(stderr, "cannot retrieve cmd line");
_exit (EXIT_FAILURE); _exit (EXIT_FAILURE);
} }
// Even if unused, this is needed to ensure we properly free the memory
argv0 = argv[0];
if (geteuid () != 0 || getenv ("_CONTAINERS_USERNS_CONFIGURED") == NULL) if (geteuid () != 0 || getenv ("_CONTAINERS_USERNS_CONFIGURED") == NULL)
do_preexec_hooks(argv, argc); do_preexec_hooks(argv, argc);

View File

@ -0,0 +1,9 @@
cdi devices
===========
This test copies a CDI device file on a tmpfs mounted on /etc/cdi, then checks that the CDI device in the compose file is present in a container. The test is skipped when running as rootless.
Validation
------------
* The CDI device is present in the container.

View File

@ -0,0 +1,14 @@
{
"cdiVersion": "0.3.0",
"kind": "vendor.com/device",
"devices": [
{
"name": "myKmsg",
"containerEdits": {
"mounts": [
{"hostPath": "/dev/kmsg", "containerPath": "/dev/kmsg1", "options": ["rw", "rprivate", "rbind"]}
]
}
}
]
}

View File

@ -0,0 +1,15 @@
services:
test:
image: alpine
command: ["top"]
volumes:
- /dev:/dev-host
security_opt:
- label=disable
deploy:
resources:
reservations:
devices:
- driver: cdi
device_ids: ['vendor.com/device=myKmsg']
capabilities: []

View File

@ -0,0 +1,9 @@
if is_rootless; then
reason=" - can't write to /etc/cdi"
_show_ok skip "$testname # skip$reason"
exit 0
fi
mkdir -p /etc/cdi
mount -t tmpfs tmpfs /etc/cdi
cp device.json /etc/cdi

View File

@ -0,0 +1,3 @@
if ! is_rootless; then
umount -l /etc/cdi
fi

View File

@ -0,0 +1,11 @@
# -*- bash -*-
ctr_name="cdi_device-test-1"
podman exec "$ctr_name" sh -c 'stat -c "%t:%T" /dev-host/kmsg'
expected=$output
podman exec "$ctr_name" sh -c 'stat -c "%t:%T" /dev/kmsg1'
is "$output" "$expected" "$testname : device /dev/kmsg1 has the same rdev as /dev/kmsg on the host"

View File

@ -22,7 +22,7 @@ var _ = Describe("Podman artifact", func() {
artifact1File, err := createArtifactFile(4192) artifact1File, err := createArtifactFile(4192)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
artifact1Name := "localhost/test/artifact1" artifact1Name := "localhost/test/artifact1"
podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...) add1 := podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...)
artifact2File, err := createArtifactFile(10240) artifact2File, err := createArtifactFile(10240)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
@ -43,6 +43,24 @@ var _ = Describe("Podman artifact", func() {
// Make sure the names are what we expect // Make sure the names are what we expect
Expect(output).To(ContainElement(artifact1Name)) Expect(output).To(ContainElement(artifact1Name))
Expect(output).To(ContainElement(artifact2Name)) Expect(output).To(ContainElement(artifact2Name))
// Check default digest length (should be 12)
defaultFormatSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls", "--format", "{{.Digest}}"}...)
defaultOutput := defaultFormatSession.OutputToStringArray()[0]
Expect(defaultOutput).To(HaveLen(12))
// Check with --no-trunc and verify the len of the digest is the same as the len what was returned when the artifact
// was added
noTruncSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls", "--no-trunc", "--format", "{{.Digest}}"}...)
truncOutput := noTruncSession.OutputToStringArray()[0]
Expect(truncOutput).To(HaveLen(len(add1.OutputToString())))
// check with --noheading and verify the header is not present through a line count AND substring match
noHeaderSession := podmanTest.PodmanExitCleanly([]string{"artifact", "ls", "--noheading"}...)
noHeaderOutput := noHeaderSession.OutputToStringArray()
Expect(noHeaderOutput).To(HaveLen(2))
Expect(noHeaderOutput).ToNot(ContainElement("REPOSITORY"))
}) })
It("podman artifact simple add", func() { It("podman artifact simple add", func() {
@ -67,6 +85,31 @@ var _ = Describe("Podman artifact", func() {
Expect(addAgain.ErrorToString()).To(Equal(fmt.Sprintf("Error: artifact %s already exists", artifact1Name))) Expect(addAgain.ErrorToString()).To(Equal(fmt.Sprintf("Error: artifact %s already exists", artifact1Name)))
}) })
It("podman artifact add with options", func() {
artifact1Name := "localhost/test/artifact1"
artifact1File, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred())
artifactType := "octet/foobar"
annotation1 := "color=blue"
annotation2 := "flavor=lemon"
podmanTest.PodmanExitCleanly([]string{"artifact", "add", "--type", artifactType, "--annotation", annotation1, "--annotation", annotation2, artifact1Name, artifact1File}...)
inspectSingleSession := podmanTest.PodmanExitCleanly([]string{"artifact", "inspect", artifact1Name}...)
a := libartifact.Artifact{}
err = json.Unmarshal([]byte(inspectSingleSession.OutputToString()), &a)
Expect(err).ToNot(HaveOccurred())
Expect(a.Name).To(Equal(artifact1Name))
Expect(a.Manifest.ArtifactType).To(Equal(artifactType))
Expect(a.Manifest.Layers[0].Annotations["color"]).To(Equal("blue"))
Expect(a.Manifest.Layers[0].Annotations["flavor"]).To(Equal("lemon"))
failSession := podmanTest.Podman([]string{"artifact", "add", "--annotation", "org.opencontainers.image.title=foobar", "foobar", artifact1File})
failSession.WaitWithDefaultTimeout()
Expect(failSession).Should(Exit(125))
Expect(failSession.ErrorToString()).Should(Equal("Error: cannot override filename with org.opencontainers.image.title annotation"))
})
It("podman artifact add multiple", func() { It("podman artifact add multiple", func() {
artifact1File1, err := createArtifactFile(1024) artifact1File1, err := createArtifactFile(1024)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
@ -85,11 +128,7 @@ var _ = Describe("Podman artifact", func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
Expect(a.Name).To(Equal(artifact1Name)) Expect(a.Name).To(Equal(artifact1Name))
var layerCount int Expect(a.Manifest.Layers).To(HaveLen(2))
for _, layer := range a.Manifests {
layerCount += len(layer.Layers)
}
Expect(layerCount).To(Equal(2))
}) })
It("podman artifact push and pull", func() { It("podman artifact push and pull", func() {

View File

@ -28,6 +28,7 @@ import (
"github.com/containers/podman/v5/pkg/inspect" "github.com/containers/podman/v5/pkg/inspect"
. "github.com/containers/podman/v5/test/utils" . "github.com/containers/podman/v5/test/utils"
"github.com/containers/podman/v5/utils" "github.com/containers/podman/v5/utils"
"github.com/containers/storage/pkg/ioutils"
"github.com/containers/storage/pkg/lockfile" "github.com/containers/storage/pkg/lockfile"
"github.com/containers/storage/pkg/reexec" "github.com/containers/storage/pkg/reexec"
"github.com/containers/storage/pkg/stringid" "github.com/containers/storage/pkg/stringid"
@ -1187,19 +1188,21 @@ func (p *PodmanTestIntegration) makeOptions(args []string, options PodmanExecOpt
} }
func writeConf(conf []byte, confPath string) { func writeConf(conf []byte, confPath string) {
if _, err := os.Stat(filepath.Dir(confPath)); os.IsNotExist(err) { GinkgoHelper()
if err := os.MkdirAll(filepath.Dir(confPath), 0o777); err != nil { err := os.MkdirAll(filepath.Dir(confPath), 0o755)
GinkgoWriter.Println(err) Expect(err).ToNot(HaveOccurred())
}
} err = ioutils.AtomicWriteFile(confPath, conf, 0o644)
if err := os.WriteFile(confPath, conf, 0o777); err != nil { Expect(err).ToNot(HaveOccurred())
GinkgoWriter.Println(err)
}
} }
func removeConf(confPath string) { func removeConf(confPath string) {
if err := os.Remove(confPath); err != nil { GinkgoHelper()
GinkgoWriter.Println(err) err := os.Remove(confPath)
// Network remove test will remove the config and then this can fail.
// If the config does not exists no reason to hard error here.
if !errors.Is(err, os.ErrNotExist) {
Expect(err).ToNot(HaveOccurred())
} }
} }

View File

@ -291,4 +291,16 @@ var _ = Describe("Podman Info", func() {
Expect(info).ToNot(ExitCleanly()) Expect(info).ToNot(ExitCleanly())
podmanTest.StartRemoteService() // Start service again so teardown runs clean podmanTest.StartRemoteService() // Start service again so teardown runs clean
}) })
It("Podman info: check client information", func() {
info := podmanTest.Podman([]string{"info", "--format", "{{ .Client }}"})
info.WaitWithDefaultTimeout()
Expect(info).To(ExitCleanly())
// client info should only appear when using the remote client
if IsRemote() {
Expect(info.OutputToString()).ToNot(Equal("<nil>"))
} else {
Expect(info.OutputToString()).To(Equal("<nil>"))
}
})
}) })