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
####
# 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"

View File

@ -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) \

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 `--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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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}}
`

View File

@ -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"

View File

@ -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

View File

@ -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*

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:
####> 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**

View File

@ -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

View File

@ -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
```

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.
#### **--no-trunc**
Do not truncate the output (default *false*).
@@option no-trunc
@@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.
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`)

View File

@ -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
}

View File

@ -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))

View File

@ -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
}

View File

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

View File

@ -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)

View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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"`
}

View File

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

View File

@ -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

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() {
i := initMachine{}
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 {
recoveryUnit := parser.NewUnitFile()
recoveryUnit.Add("Unit", "Description", "Verifies health of network and recovers if necessary")

View File

@ -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

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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);

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)
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() {

View File

@ -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())
}
}

View File

@ -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("<nil>"))
} else {
Expect(info.OutputToString()).To(Equal("<nil>"))
}
})
})