diff --git a/.cirrus.yml b/.cirrus.yml index 2061952b2b..5c7545ea8e 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -6,7 +6,7 @@ env: #### Global variables used for all tasks #### # 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. GOPROXY: "https://proxy.golang.org,direct" GOSUMDB: "sum.golang.org" diff --git a/Makefile b/Makefile index 7c85d61dd8..49b3ff3831 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,7 @@ GOFLAGS ?= -trimpath LDFLAGS_PODMAN ?= \ $(if $(GIT_COMMIT),-X $(LIBPOD)/define.gitCommit=$(GIT_COMMIT),) \ $(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._etcDir=$(ETCDIR) \ -X $(PROJECT)/v5/pkg/systemd/quadlet._binDir=$(BINDIR) \ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index acfd7e0ea1..1d8b7d63ef 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -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 `--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 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)). - 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)). @@ -23,6 +24,7 @@ ### Changes - 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. +- 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 - 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 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 `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 - 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 - 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. +- 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 ### Security diff --git a/cmd/podman/artifact/add.go b/cmd/podman/artifact/add.go index 3f3d2fb286..a92018fb3f 100644 --- a/cmd/podman/artifact/add.go +++ b/cmd/podman/artifact/add.go @@ -3,15 +3,17 @@ package artifact import ( "fmt" + "github.com/containers/common/pkg/completion" "github.com/containers/podman/v5/cmd/podman/common" "github.com/containers/podman/v5/cmd/podman/registry" "github.com/containers/podman/v5/pkg/domain/entities" + "github.com/containers/podman/v5/pkg/domain/utils" "github.com/spf13/cobra" ) var ( addCmd = &cobra.Command{ - Use: "add ARTIFACT PATH [...PATH]", + Use: "add [options] ARTIFACT PATH [...PATH]", Short: "Add an OCI artifact to the local store", Long: "Add an OCI artifact to the local store from the local filesystem", RunE: add, @@ -22,15 +24,41 @@ var ( } ) +type artifactAddOptions struct { + ArtifactType string + Annotations []string +} + +var ( + addOpts artifactAddOptions +) + func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Command: addCmd, 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 { - 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 { return err } diff --git a/cmd/podman/artifact/list.go b/cmd/podman/artifact/list.go index e9ad56542b..e2bd843ff6 100644 --- a/cmd/podman/artifact/list.go +++ b/cmd/podman/artifact/list.go @@ -31,7 +31,9 @@ var ( ) type listFlagType struct { - format string + format string + noHeading bool + noTrunc bool } type artifactListOutput struct { @@ -54,6 +56,8 @@ func init() { formatFlagName := "format" flags.StringVar(&listFlag.format, formatFlagName, defaultArtifactListOutputFormat, "Format volume output using JSON or a Go template") _ = 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 { @@ -95,10 +99,15 @@ func outputTemplate(cmd *cobra.Command, lrs []*entities.ArtifactListReport) erro if err != nil { 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{ - Digest: artifactDigest.Encoded(), + Digest: artifactHash, Repository: named.Name(), Size: units.HumanSize(float64(lr.Artifact.TotalSizeBytes())), Tag: tag, @@ -125,7 +134,7 @@ func outputTemplate(cmd *cobra.Command, lrs []*entities.ArtifactListReport) erro return err } - if rpt.RenderHeaders { + if rpt.RenderHeaders && !listFlag.noHeading { if err := rpt.Execute(headers); err != nil { return fmt.Errorf("failed to write report column headers: %w", err) } diff --git a/cmd/podman/client.go b/cmd/podman/client.go index b42a43520d..46c889b955 100644 --- a/cmd/podman/client.go +++ b/cmd/podman/client.go @@ -3,9 +3,10 @@ package main import "github.com/containers/podman/v5/libpod/define" type clientInfo struct { - OSArch string `json:"OS"` - Provider string `json:"provider"` - Version string `json:"version"` + OSArch string `json:"OS"` + Provider string `json:"provider"` + Version string `json:"version"` + BuildOrigin string `json:"buildOrigin,omitempty" yaml:",omitempty"` } func getClientInfo() (*clientInfo, error) { @@ -18,8 +19,9 @@ func getClientInfo() (*clientInfo, error) { return nil, err } return &clientInfo{ - OSArch: vinfo.OsArch, - Provider: p, - Version: vinfo.Version, + OSArch: vinfo.OsArch, + Provider: p, + Version: vinfo.Version, + BuildOrigin: vinfo.BuildOrigin, }, nil } diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 3476f33be8..d4a8058bc7 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -3,6 +3,7 @@ package common import ( "bufio" "fmt" + "io/fs" "os" "path" "path/filepath" @@ -1646,8 +1647,8 @@ func AutocompleteContainersConfModules(cmd *cobra.Command, args []string, toComp for _, d := range dirs { cleanedD := filepath.Clean(d) moduleD := cleanedD + string(os.PathSeparator) - _ = filepath.Walk(d, - func(path string, f os.FileInfo, err error) error { + _ = filepath.WalkDir(d, + func(path string, d fs.DirEntry, err error) error { if err != nil { return err } @@ -1657,7 +1658,7 @@ func AutocompleteContainersConfModules(cmd *cobra.Command, args []string, toComp return nil } - if filepath.Clean(path) == cleanedD || f.IsDir() { + if filepath.Clean(path) == cleanedD || d.IsDir() { return nil } diff --git a/cmd/podman/machine/init.go b/cmd/podman/machine/init.go index 69afdc02c6..a3db264c2f 100644 --- a/cmd/podman/machine/init.go +++ b/cmd/podman/machine/init.go @@ -62,6 +62,10 @@ func init() { ) _ = 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" flags.Uint64Var( &initOpts.DiskSize, diff --git a/cmd/podman/system/info.go b/cmd/podman/system/info.go index 071e2021ba..47c47ae539 100644 --- a/cmd/podman/system/info.go +++ b/cmd/podman/system/info.go @@ -45,6 +45,11 @@ var ( debug bool ) +type infoReport struct { + define.Info + Client *define.Version `json:",omitempty" yaml:",omitempty"` +} + func init() { registry.Commands = append(registry.Commands, registry.CliCommand{ Command: infoCommand, @@ -74,12 +79,21 @@ func info(cmd *cobra.Command, args []string) error { if err != nil { 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 { case report.IsJSON(inFormat): - b, err := json.MarshalIndent(info, "", " ") + b, err := json.MarshalIndent(infoReport, "", " ") if err != nil { return err } @@ -94,9 +108,9 @@ func info(cmd *cobra.Command, args []string) error { if err != nil { return err } - return rpt.Execute(info) + return rpt.Execute(infoReport) default: - b, err := yaml.Marshal(info) + b, err := yaml.Marshal(infoReport) if err != nil { return err } diff --git a/cmd/podman/system/version.go b/cmd/podman/system/version.go index 48ea151135..eb70b1ef72 100644 --- a/cmd/podman/system/version.go +++ b/cmd/podman/system/version.go @@ -97,6 +97,7 @@ API Version:\t{{.APIVersion}} Go Version:\t{{.GoVersion}} {{if .GitCommit -}}Git Commit:\t{{.GitCommit}}\n{{end -}} Built:\t{{.BuiltTime}} +{{if .BuildOrigin -}}Build Origin:\t{{.BuildOrigin}}\n{{end -}} OS/Arch:\t{{.OsArch}} {{- end}} @@ -108,6 +109,7 @@ API Version:\t{{.APIVersion}} Go Version:\t{{.GoVersion}} {{if .GitCommit -}}Git Commit:\t{{.GitCommit}}\n{{end -}} Built:\t{{.BuiltTime}} +{{if .BuildOrigin -}}Build Origin:\t{{.BuildOrigin}}\n{{end -}} OS/Arch:\t{{.OsArch}} {{- end}}{{- end}} ` diff --git a/contrib/pkginstaller/package.sh b/contrib/pkginstaller/package.sh index 42528c96c5..1b34001f6b 100755 --- a/contrib/pkginstaller/package.sh +++ b/contrib/pkginstaller/package.sh @@ -9,6 +9,7 @@ PRODUCTSIGN_IDENTITY=${PRODUCTSIGN_IDENTITY:-mock} NO_CODESIGN=${NO_CODESIGN:-0} HELPER_BINARIES_DIR="/opt/podman/bin" MACHINE_POLICY_JSON_DIR="/opt/podman/config" +BUILD_ORIGIN="pkginstaller" tmpBin="contrib/pkginstaller/tmp-bin" @@ -47,7 +48,7 @@ function build_podman() { } 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 mkdir -p "${tmpBin}" cp bin/darwin/podman "${tmpBin}/podman-$1" diff --git a/docs/source/markdown/.gitignore b/docs/source/markdown/.gitignore index 370b2e81c9..3dd3b2e7d4 100644 --- a/docs/source/markdown/.gitignore +++ b/docs/source/markdown/.gitignore @@ -1,3 +1,5 @@ +podman-artifact-add.1.md +podman-artifact-ls.1.md podman-artifact-pull.1.md podman-artifact-push.1.md podman-attach.1.md diff --git a/docs/source/markdown/options/annotation.manifest.md b/docs/source/markdown/options/annotation.manifest.md index 472fc40b40..e5b931dae1 100644 --- a/docs/source/markdown/options/annotation.manifest.md +++ b/docs/source/markdown/options/annotation.manifest.md @@ -1,5 +1,5 @@ ####> 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 ####> are applicable to all of those. #### **--annotation**=*annotation=value* diff --git a/docs/source/markdown/options/no-trunc.md b/docs/source/markdown/options/no-trunc.md new file mode 100644 index 0000000000..887893934d --- /dev/null +++ b/docs/source/markdown/options/no-trunc.md @@ -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*). diff --git a/docs/source/markdown/options/noheading.md b/docs/source/markdown/options/noheading.md index c07212a3b8..623d21180a 100644 --- a/docs/source/markdown/options/noheading.md +++ b/docs/source/markdown/options/noheading.md @@ -1,5 +1,5 @@ ####> 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 ####> are applicable to all of those. #### **--noheading**, **-n** diff --git a/docs/source/markdown/podman-artifact-add.1.md b/docs/source/markdown/podman-artifact-add.1.md.in similarity index 82% rename from docs/source/markdown/podman-artifact-add.1.md rename to docs/source/markdown/podman-artifact-add.1.md.in index 6ad757a3e4..35a99f9958 100644 --- a/docs/source/markdown/podman-artifact-add.1.md +++ b/docs/source/markdown/podman-artifact-add.1.md.in @@ -19,10 +19,15 @@ added. ## OPTIONS +@@option annotation.manifest + #### **--help** Print usage statement. +#### **--type** + +Set a type for the artifact being added. ## EXAMPLES @@ -39,6 +44,10 @@ $ podman artifact add quay.io/myartifact/myml:latest /tmp/foobar1.ml /tmp/foobar 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 diff --git a/docs/source/markdown/podman-artifact-ls.1.md b/docs/source/markdown/podman-artifact-ls.1.md.in similarity index 64% rename from docs/source/markdown/podman-artifact-ls.1.md rename to docs/source/markdown/podman-artifact-ls.1.md.in index 720c1dec36..552df70424 100644 --- a/docs/source/markdown/podman-artifact-ls.1.md +++ b/docs/source/markdown/podman-artifact-ls.1.md.in @@ -28,24 +28,40 @@ Print results with a Go template. | .Size | Size artifact in human readable units | | .Tag | Tag of the artifact name | +@@option no-trunc +@@option noheading ## EXAMPLES List artifacts in the local store ``` $ podman artifact ls -REPOSITORY TAG DIGEST SIZE -quay.io/artifact/foobar1 latest ab609fad386df1433f461b0643d9cf575560baf633809dcc9c190da6cc3a3c29 2.097GB -quay.io/artifact/foobar2 special cd734b558ceb8ccc0281ca76530e1dea1eb479407d3163f75fb601bffb6f73d0 12.58MB - - +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/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 ``` $ podman artifact ls --format "{{.Digest}} {{.Size}}" -ab609fad386df1433f461b0643d9cf575560baf633809dcc9c190da6cc3a3c29 2.097GB -cd734b558ceb8ccc0281ca76530e1dea1eb479407d3163f75fb601bffb6f73d0 12.58MB +ab609fad386d 2.097GB +cd734b558ceb 12.58MB ``` diff --git a/docs/source/markdown/podman-images.1.md.in b/docs/source/markdown/podman-images.1.md.in index fda6af0479..6c26107105 100644 --- a/docs/source/markdown/podman-images.1.md.in +++ b/docs/source/markdown/podman-images.1.md.in @@ -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. -#### **--no-trunc** - -Do not truncate the output (default *false*). +@@option no-trunc @@option noheading diff --git a/docs/source/markdown/podman-machine-init.1.md.in b/docs/source/markdown/podman-machine-init.1.md.in index af603e0d4e..775362a9e9 100644 --- a/docs/source/markdown/podman-machine-init.1.md.in +++ b/docs/source/markdown/podman-machine-init.1.md.in @@ -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. 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 (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. +#### **--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** Whether this machine prefers rootful (`true`) or rootless (`false`) diff --git a/libpod/define/version.go b/libpod/define/version.go index 13a8fdb778..6832aeac38 100644 --- a/libpod/define/version.go +++ b/libpod/define/version.go @@ -16,18 +16,22 @@ var ( // BuildInfo is the time at which the binary was built // It will be populated by the Makefile. 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 type Version struct { - APIVersion string - Version string - GoVersion string - GitCommit string - BuiltTime string - Built int64 - OsArch string - Os string + APIVersion string + Version string + GoVersion string + GitCommit string + BuiltTime string + Built int64 + BuildOrigin string `json:",omitempty" yaml:",omitempty"` + OsArch string + Os string } // GetVersion returns a VersionOutput struct for API and podman @@ -43,13 +47,14 @@ func GetVersion() (Version, error) { } } return Version{ - APIVersion: version.APIVersion[version.Libpod][version.CurrentAPI].String(), - Version: version.Version.String(), - GoVersion: runtime.Version(), - GitCommit: gitCommit, - BuiltTime: time.Unix(buildTime, 0).Format(time.ANSIC), - Built: buildTime, - OsArch: runtime.GOOS + "/" + runtime.GOARCH, - Os: runtime.GOOS, + APIVersion: version.APIVersion[version.Libpod][version.CurrentAPI].String(), + Version: version.Version.String(), + GoVersion: runtime.Version(), + GitCommit: gitCommit, + BuiltTime: time.Unix(buildTime, 0).Format(time.ANSIC), + Built: buildTime, + BuildOrigin: buildOrigin, + OsArch: runtime.GOOS + "/" + runtime.GOARCH, + Os: runtime.GOOS, }, nil } diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 14b37804f3..02253b9b2a 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -163,6 +163,11 @@ func cliOpts(cc handlers.CreateContainerConfig, rtc *config.Config) (*entities.C for _, dev := range cc.HostConfig.Devices { 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 readBps := make([]string, 0, len(cc.HostConfig.BlkioDeviceReadBps)) diff --git a/pkg/api/handlers/libpod/manifests.go b/pkg/api/handlers/libpod/manifests.go index ca1ae26673..3aaf0fd33e 100644 --- a/pkg/api/handlers/libpod/manifests.go +++ b/pkg/api/handlers/libpod/manifests.go @@ -29,6 +29,7 @@ import ( "github.com/containers/podman/v5/pkg/channel" "github.com/containers/podman/v5/pkg/domain/entities" "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/gorilla/mux" "github.com/gorilla/schema" @@ -520,24 +521,17 @@ func ManifestModify(w http.ResponseWriter, r *http.Request) { 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.Annotations) != 0 { utils.Error(w, http.StatusBadRequest, fmt.Errorf("can not set both Annotation and Annotations")) 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 } 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")) 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 } diff --git a/pkg/domain/entities/artifact.go b/pkg/domain/entities/artifact.go index 29f788a594..5103401e8e 100644 --- a/pkg/domain/entities/artifact.go +++ b/pkg/domain/entities/artifact.go @@ -9,7 +9,8 @@ import ( "github.com/opencontainers/go-digest" ) -type ArtifactAddoptions struct { +type ArtifactAddOptions struct { + Annotations map[string]string ArtifactType string } diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index 04b215ac82..d93883b68b 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -9,7 +9,7 @@ import ( ) 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) ArtifactList(ctx context.Context, opts ArtifactListOptions) ([]*ArtifactListReport, error) ArtifactPull(ctx context.Context, name string, opts ArtifactPullOptions) (*ArtifactPullReport, error) diff --git a/pkg/domain/infra/abi/artifact.go b/pkg/domain/infra/abi/artifact.go index 126db79e26..fe1c3c12c8 100644 --- a/pkg/domain/infra/abi/artifact.go +++ b/pkg/domain/infra/abi/artifact.go @@ -11,6 +11,7 @@ import ( "github.com/containers/common/libimage" "github.com/containers/podman/v5/pkg/domain/entities" "github.com/containers/podman/v5/pkg/libartifact/store" + "github.com/containers/podman/v5/pkg/libartifact/types" ) 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) 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()) if err != nil { 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 { return nil, err } diff --git a/pkg/domain/infra/tunnel/artifact.go b/pkg/domain/infra/tunnel/artifact.go index 99657707dd..c404da95f0 100644 --- a/pkg/domain/infra/tunnel/artifact.go +++ b/pkg/domain/infra/tunnel/artifact.go @@ -9,7 +9,7 @@ import ( // 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") } @@ -33,6 +33,6 @@ func (ir *ImageEngine) ArtifactPush(ctx context.Context, name string, opts entit 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") } diff --git a/pkg/domain/utils/utils.go b/pkg/domain/utils/utils.go index ca7a3210fb..e1bd488637 100644 --- a/pkg/domain/utils/utils.go +++ b/pkg/domain/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "fmt" "net/url" "strings" @@ -39,3 +40,17 @@ func ToURLValues(f []string) (filters url.Values) { } 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 +} diff --git a/pkg/libartifact/artifact.go b/pkg/libartifact/artifact.go index 326ea72f2c..7d8304e533 100644 --- a/pkg/libartifact/artifact.go +++ b/pkg/libartifact/artifact.go @@ -11,17 +11,17 @@ import ( ) type Artifact struct { - Manifests []manifest.OCI1 - Name string + // 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 } // TotalSizeBytes returns the total bytes of the all the artifact layers func (a *Artifact) TotalSizeBytes() int64 { var s int64 - for _, artifact := range a.Manifests { - for _, layer := range artifact.Layers { - s += layer.Size - } + for _, layer := range a.Manifest.Layers { + s += layer.Size } return s } @@ -45,13 +45,7 @@ func (a *Artifact) SetName(name string) { } func (a *Artifact) GetDigest() (*digest.Digest, error) { - if len(a.Manifests) > 1 { - 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]) + b, err := json.Marshal(a.Manifest) if err != nil { return nil, err } @@ -72,17 +66,13 @@ func (al ArtifactList) GetByNameOrDigest(nameOrDigest string) (*Artifact, bool, } // Before giving up, check by digest 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() - if err != nil { - return nil, false, err - } - // If the artifact's digest matches or is a prefix of ... - if artifactDigest.Encoded() == nameOrDigest || strings.HasPrefix(artifactDigest.Encoded(), nameOrDigest) { - return artifact, true, nil - } + artifactDigest, err := artifact.GetDigest() + if err != nil { + return nil, false, err + } + // If the artifact's digest matches or is a prefix of ... + if artifactDigest.Encoded() == nameOrDigest || strings.HasPrefix(artifactDigest.Encoded(), nameOrDigest) { + return artifact, true, nil } } return nil, false, fmt.Errorf("no artifact found with name or digest of %s", nameOrDigest) diff --git a/pkg/libartifact/store/store.go b/pkg/libartifact/store/store.go index c33e17d9ef..8a0364c247 100644 --- a/pkg/libartifact/store/store.go +++ b/pkg/libartifact/store/store.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "os" "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 // 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 { return nil, ErrEmptyArtifactName } @@ -191,6 +193,13 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _ defer imageDest.Close() 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 newBlobDigest, newBlobSize, err := layout.PutBlobFromLocalFile(ctx, imageDest, path) if err != nil { @@ -200,14 +209,16 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _ if err != nil { return nil, err } - newArtifactAnnotations := map[string]string{} - newArtifactAnnotations[specV1.AnnotationTitle] = filepath.Base(path) + + annots[specV1.AnnotationTitle] = filepath.Base(path) + newLayer := specV1.Descriptor{ MediaType: detectedType, Digest: newBlobDigest, Size: newBlobSize, - Annotations: newArtifactAnnotations, + Annotations: annots, } + artifactManifestLayers = append(artifactManifestLayers, newLayer) } @@ -215,11 +226,12 @@ func (as ArtifactStore) Add(ctx context.Context, dest string, paths []string, _ Versioned: specs.Versioned{SchemaVersion: 2}, MediaType: specV1.MediaTypeImageManifest, // TODO This should probably be configurable once the CLI is capable - ArtifactType: "", - Config: specV1.DescriptorEmptyJSON, - Layers: artifactManifestLayers, + Config: specV1.DescriptorEmptyJSON, + Layers: artifactManifestLayers, } + artifactManifest.ArtifactType = options.ArtifactType + rawData, err := json.Marshal(artifactManifest) if err != nil { return nil, err @@ -287,13 +299,13 @@ func (as ArtifactStore) getArtifacts(ctx context.Context, _ *libartTypes.GetArti if err != nil { return nil, err } - manifests, err := getManifests(ctx, imgSrc, nil) + manifest, err := getManifest(ctx, imgSrc) imgSrc.Close() if err != nil { return nil, err } artifact := libartifact.Artifact{ - Manifests: manifests, + Manifest: manifest, } if val, ok := l.ManifestDescriptor.Annotations[specV1.AnnotationRefName]; ok { artifact.SetName(val) @@ -304,41 +316,25 @@ func (as ArtifactStore) getArtifacts(ctx context.Context, _ *libartTypes.GetArti return al, nil } -// getManifests takes an imgSrc and starting digest (nil means "top") and collects all the manifests "under" -// it. this func calls itself recursively with a new startingDigest assuming that we are dealing with -// an index list -func getManifests(ctx context.Context, imgSrc types.ImageSource, startingDigest *digest.Digest) ([]manifest.OCI1, error) { - var ( - manifests []manifest.OCI1 - ) - b, manifestType, err := imgSrc.GetManifest(ctx, startingDigest) +// getManifest takes an imgSrc and returns the manifest for the imgSrc. +// A OCI index list is not supported and will return an error. +func getManifest(ctx context.Context, imgSrc types.ImageSource) (*manifest.OCI1, error) { + b, manifestType, err := imgSrc.GetManifest(ctx, nil) if err != nil { return nil, err } - // this assumes that there are only single, and multi-images - if !manifest.MIMETypeIsMultiImage(manifestType) { - // these are the keepers - mani, err := manifest.OCI1FromManifest(b) - if err != nil { - return nil, err - } - manifests = append(manifests, *mani) - return manifests, nil + // We only support a single flat manifest and not an oci index list + if manifest.MIMETypeIsMultiImage(manifestType) { + return nil, fmt.Errorf("manifest %q is index list", imgSrc.Reference().StringWithinTransport()) } - // We are dealing with an oci index list - maniList, err := manifest.OCI1IndexFromManifest(b) + + // parse the single manifest + mani, err := manifest.OCI1FromManifest(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 + return mani, nil } func createEmptyStanza(path string) error { diff --git a/pkg/libartifact/types/config.go b/pkg/libartifact/types/config.go index c52d677942..c458e646a6 100644 --- a/pkg/libartifact/types/config.go +++ b/pkg/libartifact/types/config.go @@ -3,3 +3,9 @@ package types // GetArtifactOptions is a struct containing options that for obtaining artifacts. // It is meant for future growth or changes required without wacking the API 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"` +} diff --git a/pkg/machine/define/initopts.go b/pkg/machine/define/initopts.go index 4ddf87ca19..b1ccaaa6b7 100644 --- a/pkg/machine/define/initopts.go +++ b/pkg/machine/define/initopts.go @@ -3,6 +3,7 @@ package define import "net/url" type InitOptions struct { + PlaybookPath string CPUS uint64 DiskSize uint64 IgnitionPath string diff --git a/pkg/machine/e2e/config_init_test.go b/pkg/machine/e2e/config_init_test.go index 0423010477..031f4395f7 100644 --- a/pkg/machine/e2e/config_init_test.go +++ b/pkg/machine/e2e/config_init_test.go @@ -11,19 +11,21 @@ import ( type initMachine struct { /* - --cpus uint Number of CPUs (default 1) - --disk-size uint Disk size in GiB (default 100) - --ignition-path string Path to ignition file - --username string Username of the remote user (default "core" for FCOS, "user" for Fedora) - --image-path string Path to bootable image (default "testing") - -m, --memory uint Memory in MiB (default 2048) - --now Start machine now - --rootful Whether this machine should prefer rootful container execution - --timezone string Set timezone (default "local") - -v, --volume stringArray Volumes to mount, source:target - --volume-driver string Optional volume driver + --cpus uint Number of CPUs (default 1) + --disk-size uint Disk size in GiB (default 100) + --ignition-path string Path to ignition file + --username string Username of the remote user (default "core" for FCOS, "user" for Fedora) + --image-path string Path to bootable image (default "testing") + -m, --memory uint Memory in MiB (default 2048) + --now Start machine now + --rootful Whether this machine should prefer rootful container execution + --playbook string Run an ansible playbook after first boot + --timezone string Set timezone (default "local") + -v, --volume stringArray Volumes to mount, source:target + --volume-driver string Optional volume driver */ + playbook string cpus *uint diskSize *uint ignitionPath string @@ -73,6 +75,9 @@ func (i *initMachine) buildCmd(m *machineTestBuilder) []string { if i.rootful { cmd = append(cmd, "--rootful") } + if l := len(i.playbook); l > 0 { + cmd = append(cmd, "--playbook", i.playbook) + } if i.userModeNetworking { cmd = append(cmd, "--user-mode-networking") } @@ -152,6 +157,11 @@ func (i *initMachine) withRootful(r bool) *initMachine { return i } +func (i *initMachine) withRunPlaybook(p string) *initMachine { + i.playbook = p + return i +} + func (i *initMachine) withUserModeNetworking(r bool) *initMachine { //nolint:unused i.userModeNetworking = r return i diff --git a/pkg/machine/e2e/init_test.go b/pkg/machine/e2e/init_test.go index bd5fe683c5..8e3bc72b92 100644 --- a/pkg/machine/e2e/init_test.go +++ b/pkg/machine/e2e/init_test.go @@ -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() { i := initMachine{} session, err := mb.setCmd(i.withImage(mb.imagePath)).run() diff --git a/pkg/machine/ignition/ignition.go b/pkg/machine/ignition/ignition.go index 62bf7a872f..3995ef714c 100644 --- a/pkg/machine/ignition/ignition.go +++ b/pkg/machine/ignition/ignition.go @@ -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 { recoveryUnit := parser.NewUnitFile() recoveryUnit.Add("Unit", "Description", "Verifies health of network and recovers if necessary") diff --git a/pkg/machine/shim/host.go b/pkg/machine/shim/host.go index 1315c9adb9..82e70be8fa 100644 --- a/pkg/machine/shim/host.go +++ b/pkg/machine/shim/host.go @@ -4,6 +4,7 @@ import ( "bufio" "errors" "fmt" + "io" "os" "path/filepath" "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) if err != nil { 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 if mp.UseProviderNetworkSetup() { return nil diff --git a/pkg/machine/vmconfigs/config.go b/pkg/machine/vmconfigs/config.go index 417d5b3fe1..a45fee6432 100644 --- a/pkg/machine/vmconfigs/config.go +++ b/pkg/machine/vmconfigs/config.go @@ -53,6 +53,8 @@ type MachineConfig struct { Starting bool Rosetta bool + + Ansible *AnsibleConfig } type machineImage interface { //nolint:unused @@ -148,3 +150,9 @@ type VMStats struct { // LastUp contains the last recorded uptime LastUp time.Time } + +type AnsibleConfig struct { + PlaybookPath string + Contents string + User string +} diff --git a/pkg/machine/wsl/machine.go b/pkg/machine/wsl/machine.go index 38c1a91261..3355f6b2b5 100644 --- a/pkg/machine/wsl/machine.go +++ b/pkg/machine/wsl/machine.go @@ -148,7 +148,7 @@ func createKeys(mc *vmconfigs.MachineConfig, dist string) error { 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 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) @@ -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) } + 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) if err := wslPipe(lingerService, dist, "sh", "-c", lingerCmd); err != nil { return fmt.Errorf("could not generate linger service for guest OS: %w", err) diff --git a/pkg/machine/wsl/stubber.go b/pkg/machine/wsl/stubber.go index 073b0b04bf..75b12fad03 100644 --- a/pkg/machine/wsl/stubber.go +++ b/pkg/machine/wsl/stubber.go @@ -68,7 +68,7 @@ func (w WSLStubber) CreateVM(opts define.CreateVMOpts, mc *vmconfigs.MachineConf } fmt.Println("Configuring system...") - if err = configureSystem(mc, dist); err != nil { + if err = configureSystem(mc, dist, mc.Ansible); err != nil { return err } diff --git a/pkg/rootless/rootless_linux.c b/pkg/rootless/rootless_linux.c index ddb97b3346..4f71d49e5c 100644 --- a/pkg/rootless/rootless_linux.c +++ b/pkg/rootless/rootless_linux.c @@ -364,7 +364,6 @@ get_cmd_line_args (int *argc_out) static bool can_use_shortcut (char **argv) { - cleanup_free char *argv0 = NULL; bool ret = true; int argc; @@ -372,8 +371,6 @@ can_use_shortcut (char **argv) return false; #endif - argv0 = argv[0]; - if (strstr (argv[0], "podman") == NULL) return false; @@ -439,6 +436,7 @@ static void __attribute__((constructor)) init() const char *listen_fds; const char *listen_fdnames; cleanup_free char **argv = NULL; + cleanup_free char *argv0 = NULL; cleanup_dir DIR *d = NULL; int argc; @@ -496,6 +494,8 @@ static void __attribute__((constructor)) init() fprintf(stderr, "cannot retrieve cmd line"); _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) do_preexec_hooks(argv, argc); diff --git a/test/compose/cdi_device/README.md b/test/compose/cdi_device/README.md new file mode 100644 index 0000000000..961af83fc7 --- /dev/null +++ b/test/compose/cdi_device/README.md @@ -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. diff --git a/test/compose/cdi_device/device.json b/test/compose/cdi_device/device.json new file mode 100644 index 0000000000..d489906914 --- /dev/null +++ b/test/compose/cdi_device/device.json @@ -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"]} + ] + } + } + ] +} diff --git a/test/compose/cdi_device/docker-compose.yml b/test/compose/cdi_device/docker-compose.yml new file mode 100644 index 0000000000..dfbeb2e906 --- /dev/null +++ b/test/compose/cdi_device/docker-compose.yml @@ -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: [] diff --git a/test/compose/cdi_device/setup.sh b/test/compose/cdi_device/setup.sh new file mode 100644 index 0000000000..224caa5443 --- /dev/null +++ b/test/compose/cdi_device/setup.sh @@ -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 diff --git a/test/compose/cdi_device/teardown.sh b/test/compose/cdi_device/teardown.sh new file mode 100644 index 0000000000..770afd1269 --- /dev/null +++ b/test/compose/cdi_device/teardown.sh @@ -0,0 +1,3 @@ +if ! is_rootless; then + umount -l /etc/cdi +fi diff --git a/test/compose/cdi_device/tests.sh b/test/compose/cdi_device/tests.sh new file mode 100644 index 0000000000..230ad1d10e --- /dev/null +++ b/test/compose/cdi_device/tests.sh @@ -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" diff --git a/test/e2e/artifact_test.go b/test/e2e/artifact_test.go index 13deb8589a..c3cfe504eb 100644 --- a/test/e2e/artifact_test.go +++ b/test/e2e/artifact_test.go @@ -22,7 +22,7 @@ var _ = Describe("Podman artifact", func() { artifact1File, err := createArtifactFile(4192) Expect(err).ToNot(HaveOccurred()) artifact1Name := "localhost/test/artifact1" - podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...) + add1 := podmanTest.PodmanExitCleanly([]string{"artifact", "add", artifact1Name, artifact1File}...) artifact2File, err := createArtifactFile(10240) Expect(err).ToNot(HaveOccurred()) @@ -43,6 +43,24 @@ var _ = Describe("Podman artifact", func() { // Make sure the names are what we expect Expect(output).To(ContainElement(artifact1Name)) 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() { @@ -67,6 +85,31 @@ var _ = Describe("Podman artifact", func() { 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() { artifact1File1, err := createArtifactFile(1024) Expect(err).ToNot(HaveOccurred()) @@ -85,11 +128,7 @@ var _ = Describe("Podman artifact", func() { Expect(err).ToNot(HaveOccurred()) Expect(a.Name).To(Equal(artifact1Name)) - var layerCount int - for _, layer := range a.Manifests { - layerCount += len(layer.Layers) - } - Expect(layerCount).To(Equal(2)) + Expect(a.Manifest.Layers).To(HaveLen(2)) }) It("podman artifact push and pull", func() { diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 898efbc889..af9d523be7 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -28,6 +28,7 @@ import ( "github.com/containers/podman/v5/pkg/inspect" . "github.com/containers/podman/v5/test/utils" "github.com/containers/podman/v5/utils" + "github.com/containers/storage/pkg/ioutils" "github.com/containers/storage/pkg/lockfile" "github.com/containers/storage/pkg/reexec" "github.com/containers/storage/pkg/stringid" @@ -1187,19 +1188,21 @@ func (p *PodmanTestIntegration) makeOptions(args []string, options PodmanExecOpt } func writeConf(conf []byte, confPath string) { - if _, err := os.Stat(filepath.Dir(confPath)); os.IsNotExist(err) { - if err := os.MkdirAll(filepath.Dir(confPath), 0o777); err != nil { - GinkgoWriter.Println(err) - } - } - if err := os.WriteFile(confPath, conf, 0o777); err != nil { - GinkgoWriter.Println(err) - } + GinkgoHelper() + err := os.MkdirAll(filepath.Dir(confPath), 0o755) + Expect(err).ToNot(HaveOccurred()) + + err = ioutils.AtomicWriteFile(confPath, conf, 0o644) + Expect(err).ToNot(HaveOccurred()) } func removeConf(confPath string) { - if err := os.Remove(confPath); err != nil { - GinkgoWriter.Println(err) + GinkgoHelper() + 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()) } } diff --git a/test/e2e/info_test.go b/test/e2e/info_test.go index 87c8902621..028ee8a6a1 100644 --- a/test/e2e/info_test.go +++ b/test/e2e/info_test.go @@ -291,4 +291,16 @@ var _ = Describe("Podman Info", func() { Expect(info).ToNot(ExitCleanly()) 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("")) + } else { + Expect(info.OutputToString()).To(Equal("")) + } + }) })