mirror of https://github.com/containers/podman.git
912 lines
28 KiB
Go
912 lines
28 KiB
Go
package abi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/containers/common/libimage"
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/image/v5/docker"
|
|
"github.com/containers/image/v5/docker/reference"
|
|
"github.com/containers/image/v5/manifest"
|
|
"github.com/containers/image/v5/pkg/compression"
|
|
"github.com/containers/image/v5/signature"
|
|
"github.com/containers/image/v5/transports"
|
|
"github.com/containers/image/v5/transports/alltransports"
|
|
"github.com/containers/podman/v4/libpod/define"
|
|
"github.com/containers/podman/v4/pkg/domain/entities"
|
|
"github.com/containers/podman/v4/pkg/domain/entities/reports"
|
|
domainUtils "github.com/containers/podman/v4/pkg/domain/utils"
|
|
"github.com/containers/podman/v4/pkg/errorhandling"
|
|
"github.com/containers/podman/v4/pkg/rootless"
|
|
"github.com/containers/storage"
|
|
dockerRef "github.com/docker/distribution/reference"
|
|
"github.com/opencontainers/go-digest"
|
|
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
func (ir *ImageEngine) Exists(_ context.Context, nameOrID string) (*entities.BoolReport, error) {
|
|
exists, err := ir.Libpod.LibimageRuntime().Exists(nameOrID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &entities.BoolReport{Value: exists}, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) ([]*reports.PruneReport, error) {
|
|
pruneOptions := &libimage.RemoveImagesOptions{
|
|
RemoveContainerFunc: ir.Libpod.RemoveContainersForImageCallback(ctx),
|
|
IsExternalContainerFunc: ir.Libpod.IsExternalContainerCallback(ctx),
|
|
ExternalContainers: opts.External,
|
|
Filters: append(opts.Filter, "readonly=false"),
|
|
WithSize: true,
|
|
}
|
|
|
|
if !opts.All {
|
|
pruneOptions.Filters = append(pruneOptions.Filters, "dangling=true")
|
|
}
|
|
if opts.External {
|
|
pruneOptions.Filters = append(pruneOptions.Filters, "containers=external")
|
|
} else {
|
|
pruneOptions.Filters = append(pruneOptions.Filters, "containers=false")
|
|
}
|
|
|
|
pruneReports := make([]*reports.PruneReport, 0)
|
|
|
|
// Now prune all images until we converge.
|
|
numPreviouslyRemovedImages := 1
|
|
for {
|
|
removedImages, rmErrors := ir.Libpod.LibimageRuntime().RemoveImages(ctx, nil, pruneOptions)
|
|
if rmErrors != nil {
|
|
return nil, errorhandling.JoinErrors(rmErrors)
|
|
}
|
|
|
|
for _, rmReport := range removedImages {
|
|
r := *rmReport
|
|
pruneReports = append(pruneReports, &reports.PruneReport{
|
|
Id: r.ID,
|
|
Size: uint64(r.Size),
|
|
})
|
|
}
|
|
|
|
numRemovedImages := len(removedImages)
|
|
if numRemovedImages+numPreviouslyRemovedImages == 0 {
|
|
break
|
|
}
|
|
numPreviouslyRemovedImages = numRemovedImages
|
|
}
|
|
|
|
return pruneReports, nil
|
|
}
|
|
|
|
func toDomainHistoryLayer(layer *libimage.ImageHistory) entities.ImageHistoryLayer {
|
|
l := entities.ImageHistoryLayer{}
|
|
l.ID = layer.ID
|
|
if layer.Created != nil {
|
|
l.Created = *layer.Created
|
|
}
|
|
l.CreatedBy = layer.CreatedBy
|
|
copy(l.Tags, layer.Tags)
|
|
l.Size = layer.Size
|
|
l.Comment = layer.Comment
|
|
return l
|
|
}
|
|
|
|
func (ir *ImageEngine) History(ctx context.Context, nameOrID string, opts entities.ImageHistoryOptions) (*entities.ImageHistoryReport, error) {
|
|
image, _, err := ir.Libpod.LibimageRuntime().LookupImage(nameOrID, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results, err := image.History(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
history := entities.ImageHistoryReport{
|
|
Layers: make([]entities.ImageHistoryLayer, len(results)),
|
|
}
|
|
|
|
for i := range results {
|
|
history.Layers[i] = toDomainHistoryLayer(&results[i])
|
|
}
|
|
return &history, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Mount(ctx context.Context, nameOrIDs []string, opts entities.ImageMountOptions) ([]*entities.ImageMountReport, error) {
|
|
if opts.All && len(nameOrIDs) > 0 {
|
|
return nil, errors.New("cannot mix --all with images")
|
|
}
|
|
|
|
if os.Geteuid() != 0 {
|
|
if driver := ir.Libpod.StorageConfig().GraphDriverName; driver != "vfs" {
|
|
// Do not allow to mount a graphdriver that is not vfs if we are creating the userns as part
|
|
// of the mount command.
|
|
return nil, fmt.Errorf("cannot mount using driver %s in rootless mode", driver)
|
|
}
|
|
|
|
became, ret, err := rootless.BecomeRootInUserNS("")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if became {
|
|
os.Exit(ret)
|
|
}
|
|
}
|
|
|
|
listImagesOptions := &libimage.ListImagesOptions{}
|
|
if opts.All {
|
|
listImagesOptions.Filters = []string{"readonly=false"}
|
|
}
|
|
images, err := ir.Libpod.LibimageRuntime().ListImages(ctx, nameOrIDs, listImagesOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mountReports := []*entities.ImageMountReport{}
|
|
listMountsOnly := !opts.All && len(nameOrIDs) == 0
|
|
for _, i := range images {
|
|
var mountPoint string
|
|
var err error
|
|
if listMountsOnly {
|
|
// We're only looking for mounted images.
|
|
mountPoint, err = i.Mountpoint()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Not mounted, so skip.
|
|
if mountPoint == "" {
|
|
continue
|
|
}
|
|
} else {
|
|
mountPoint, err = i.Mount(ctx, nil, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
tags, err := i.RepoTags()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mountReports = append(mountReports, &entities.ImageMountReport{
|
|
Id: i.ID(),
|
|
Name: string(i.Digest()),
|
|
Repositories: tags,
|
|
Path: mountPoint,
|
|
})
|
|
}
|
|
return mountReports, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Unmount(ctx context.Context, nameOrIDs []string, options entities.ImageUnmountOptions) ([]*entities.ImageUnmountReport, error) {
|
|
if options.All && len(nameOrIDs) > 0 {
|
|
return nil, errors.New("cannot mix --all with images")
|
|
}
|
|
|
|
listImagesOptions := &libimage.ListImagesOptions{}
|
|
if options.All {
|
|
listImagesOptions.Filters = []string{"readonly=false"}
|
|
}
|
|
images, err := ir.Libpod.LibimageRuntime().ListImages(ctx, nameOrIDs, listImagesOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
unmountReports := []*entities.ImageUnmountReport{}
|
|
for _, image := range images {
|
|
r := &entities.ImageUnmountReport{Id: image.ID()}
|
|
mountPoint, err := image.Mountpoint()
|
|
if err != nil {
|
|
r.Err = err
|
|
unmountReports = append(unmountReports, r)
|
|
continue
|
|
}
|
|
if mountPoint == "" {
|
|
// Skip if the image wasn't mounted.
|
|
continue
|
|
}
|
|
r.Err = image.Unmount(options.Force)
|
|
unmountReports = append(unmountReports, r)
|
|
}
|
|
return unmountReports, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Pull(ctx context.Context, rawImage string, options entities.ImagePullOptions) (*entities.ImagePullReport, error) {
|
|
pullOptions := &libimage.PullOptions{AllTags: options.AllTags}
|
|
pullOptions.AuthFilePath = options.Authfile
|
|
pullOptions.CertDirPath = options.CertDir
|
|
pullOptions.Username = options.Username
|
|
pullOptions.Password = options.Password
|
|
pullOptions.Architecture = options.Arch
|
|
pullOptions.OS = options.OS
|
|
pullOptions.Variant = options.Variant
|
|
pullOptions.SignaturePolicyPath = options.SignaturePolicy
|
|
pullOptions.InsecureSkipTLSVerify = options.SkipTLSVerify
|
|
|
|
if !options.Quiet {
|
|
pullOptions.Writer = os.Stderr
|
|
}
|
|
|
|
pulledImages, err := ir.Libpod.LibimageRuntime().Pull(ctx, rawImage, options.PullPolicy, pullOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pulledIDs := make([]string, len(pulledImages))
|
|
for i := range pulledImages {
|
|
pulledIDs[i] = pulledImages[i].ID()
|
|
}
|
|
|
|
return &entities.ImagePullReport{Images: pulledIDs}, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Inspect(ctx context.Context, namesOrIDs []string, opts entities.InspectOptions) ([]*entities.ImageInspectReport, []error, error) {
|
|
reports := []*entities.ImageInspectReport{}
|
|
errs := []error{}
|
|
|
|
inspectOptions := &libimage.InspectOptions{WithParent: true, WithSize: true}
|
|
for _, i := range namesOrIDs {
|
|
img, _, err := ir.Libpod.LibimageRuntime().LookupImage(i, nil)
|
|
if err != nil {
|
|
// This is probably a no such image, treat as nonfatal.
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
result, err := img.Inspect(ctx, inspectOptions)
|
|
if err != nil {
|
|
// This is more likely to be fatal.
|
|
return nil, nil, err
|
|
}
|
|
report := entities.ImageInspectReport{}
|
|
if err := domainUtils.DeepCopy(&report, result); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
reports = append(reports, &report)
|
|
}
|
|
return reports, errs, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) error {
|
|
var manifestType string
|
|
switch options.Format {
|
|
case "":
|
|
// Default
|
|
case "oci":
|
|
manifestType = imgspecv1.MediaTypeImageManifest
|
|
case "v2s1":
|
|
manifestType = manifest.DockerV2Schema1SignedMediaType
|
|
case "v2s2", "docker":
|
|
manifestType = manifest.DockerV2Schema2MediaType
|
|
default:
|
|
return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", options.Format)
|
|
}
|
|
|
|
pushOptions := &libimage.PushOptions{}
|
|
pushOptions.AuthFilePath = options.Authfile
|
|
pushOptions.CertDirPath = options.CertDir
|
|
pushOptions.DirForceCompress = options.Compress
|
|
pushOptions.Username = options.Username
|
|
pushOptions.Password = options.Password
|
|
pushOptions.ManifestMIMEType = manifestType
|
|
pushOptions.RemoveSignatures = options.RemoveSignatures
|
|
pushOptions.SignBy = options.SignBy
|
|
pushOptions.SignPassphrase = options.SignPassphrase
|
|
pushOptions.SignBySigstorePrivateKeyFile = options.SignBySigstorePrivateKeyFile
|
|
pushOptions.SignSigstorePrivateKeyPassphrase = options.SignSigstorePrivateKeyPassphrase
|
|
pushOptions.InsecureSkipTLSVerify = options.SkipTLSVerify
|
|
pushOptions.Writer = options.Writer
|
|
|
|
compressionFormat := options.CompressionFormat
|
|
if compressionFormat == "" {
|
|
config, err := ir.Libpod.GetConfigNoCopy()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
compressionFormat = config.Engine.CompressionFormat
|
|
}
|
|
if compressionFormat != "" {
|
|
algo, err := compression.AlgorithmByName(compressionFormat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pushOptions.CompressionFormat = &algo
|
|
}
|
|
|
|
if !options.Quiet && pushOptions.Writer == nil {
|
|
pushOptions.Writer = os.Stderr
|
|
}
|
|
|
|
pushedManifestBytes, pushError := ir.Libpod.LibimageRuntime().Push(ctx, source, destination, pushOptions)
|
|
if pushError == nil {
|
|
if options.DigestFile != "" {
|
|
manifestDigest, err := manifest.Digest(pushedManifestBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ioutil.WriteFile(options.DigestFile, []byte(manifestDigest.String()), 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
// If the image could not be found, we may be referring to a manifest
|
|
// list but could not find a matching image instance in the local
|
|
// containers storage. In that case, fall back and attempt to push the
|
|
// (entire) manifest.
|
|
if _, err := ir.Libpod.LibimageRuntime().LookupManifestList(source); err == nil {
|
|
_, err := ir.ManifestPush(ctx, source, destination, options)
|
|
return err
|
|
}
|
|
return pushError
|
|
}
|
|
func (ir *ImageEngine) Tag(ctx context.Context, nameOrID string, tags []string, options entities.ImageTagOptions) error {
|
|
// Allow tagging manifest list instead of resolving instances from manifest
|
|
lookupOptions := &libimage.LookupImageOptions{ManifestList: true}
|
|
image, _, err := ir.Libpod.LibimageRuntime().LookupImage(nameOrID, lookupOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, tag := range tags {
|
|
if err := image.Tag(tag); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Untag(ctx context.Context, nameOrID string, tags []string, options entities.ImageUntagOptions) error {
|
|
image, _, err := ir.Libpod.LibimageRuntime().LookupImage(nameOrID, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// If only one arg is provided, all names are to be untagged
|
|
if len(tags) == 0 {
|
|
tags = image.Names()
|
|
}
|
|
for _, tag := range tags {
|
|
if err := image.Untag(tag); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Load(ctx context.Context, options entities.ImageLoadOptions) (*entities.ImageLoadReport, error) {
|
|
loadOptions := &libimage.LoadOptions{}
|
|
loadOptions.SignaturePolicyPath = options.SignaturePolicy
|
|
if !options.Quiet {
|
|
loadOptions.Writer = os.Stderr
|
|
}
|
|
|
|
loadedImages, err := ir.Libpod.LibimageRuntime().Load(ctx, options.Input, loadOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &entities.ImageLoadReport{Names: loadedImages}, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Save(ctx context.Context, nameOrID string, tags []string, options entities.ImageSaveOptions) error {
|
|
saveOptions := &libimage.SaveOptions{}
|
|
saveOptions.DirForceCompress = options.Compress
|
|
saveOptions.OciAcceptUncompressedLayers = options.OciAcceptUncompressedLayers
|
|
|
|
// Force signature removal to preserve backwards compat.
|
|
// See https://github.com/containers/podman/pull/11669#issuecomment-925250264
|
|
saveOptions.RemoveSignatures = true
|
|
|
|
if !options.Quiet {
|
|
saveOptions.Writer = os.Stderr
|
|
}
|
|
|
|
names := []string{nameOrID}
|
|
if options.MultiImageArchive {
|
|
names = append(names, tags...)
|
|
} else {
|
|
saveOptions.AdditionalTags = tags
|
|
}
|
|
return ir.Libpod.LibimageRuntime().Save(ctx, names, options.Format, options.Output, saveOptions)
|
|
}
|
|
|
|
func (ir *ImageEngine) Import(ctx context.Context, options entities.ImageImportOptions) (*entities.ImageImportReport, error) {
|
|
importOptions := &libimage.ImportOptions{}
|
|
importOptions.Changes = options.Changes
|
|
importOptions.CommitMessage = options.Message
|
|
importOptions.Tag = options.Reference
|
|
importOptions.SignaturePolicyPath = options.SignaturePolicy
|
|
importOptions.OS = options.OS
|
|
importOptions.Arch = options.Architecture
|
|
importOptions.Variant = options.Variant
|
|
|
|
if !options.Quiet {
|
|
importOptions.Writer = os.Stderr
|
|
}
|
|
|
|
imageID, err := ir.Libpod.LibimageRuntime().Import(ctx, options.Source, importOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &entities.ImageImportReport{Id: imageID}, nil
|
|
}
|
|
|
|
// Search for images using term and filters
|
|
func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.ImageSearchOptions) ([]entities.ImageSearchReport, error) {
|
|
filter, err := libimage.ParseSearchFilter(opts.Filters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
searchOptions := &libimage.SearchOptions{
|
|
Authfile: opts.Authfile,
|
|
Filter: *filter,
|
|
Limit: opts.Limit,
|
|
NoTrunc: true,
|
|
InsecureSkipTLSVerify: opts.SkipTLSVerify,
|
|
ListTags: opts.ListTags,
|
|
}
|
|
|
|
searchResults, err := ir.Libpod.LibimageRuntime().Search(ctx, term, searchOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert from image.SearchResults to entities.ImageSearchReport. We don't
|
|
// want to leak any low-level packages into the remote client, which
|
|
// requires converting.
|
|
reports := make([]entities.ImageSearchReport, len(searchResults))
|
|
for i := range searchResults {
|
|
reports[i].Index = searchResults[i].Index
|
|
reports[i].Name = searchResults[i].Name
|
|
reports[i].Description = searchResults[i].Description
|
|
reports[i].Stars = searchResults[i].Stars
|
|
reports[i].Official = searchResults[i].Official
|
|
reports[i].Automated = searchResults[i].Automated
|
|
reports[i].Tag = searchResults[i].Tag
|
|
}
|
|
|
|
return reports, nil
|
|
}
|
|
|
|
// Config returns a copy of the configuration used by the runtime
|
|
func (ir *ImageEngine) Config(_ context.Context) (*config.Config, error) {
|
|
return ir.Libpod.GetConfig()
|
|
}
|
|
|
|
func (ir *ImageEngine) Build(ctx context.Context, containerFiles []string, opts entities.BuildOptions) (*entities.BuildReport, error) {
|
|
id, _, err := ir.Libpod.Build(ctx, opts.BuildOptions, containerFiles...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &entities.BuildReport{ID: id}, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Tree(ctx context.Context, nameOrID string, opts entities.ImageTreeOptions) (*entities.ImageTreeReport, error) {
|
|
image, _, err := ir.Libpod.LibimageRuntime().LookupImage(nameOrID, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tree, err := image.Tree(opts.WhatRequires)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &entities.ImageTreeReport{Tree: tree}, nil
|
|
}
|
|
|
|
// removeErrorsToExitCode returns an exit code for the specified slice of
|
|
// image-removal errors. The error codes are set according to the documented
|
|
// behaviour in the Podman man pages.
|
|
func removeErrorsToExitCode(rmErrors []error) int {
|
|
var (
|
|
// noSuchImageErrors indicates that at least one image was not found.
|
|
noSuchImageErrors bool
|
|
// inUseErrors indicates that at least one image is being used by a
|
|
// container.
|
|
inUseErrors bool
|
|
// otherErrors indicates that at least one error other than the two
|
|
// above occurred.
|
|
otherErrors bool
|
|
)
|
|
|
|
if len(rmErrors) == 0 {
|
|
return 0
|
|
}
|
|
|
|
for _, e := range rmErrors {
|
|
//nolint:gocritic
|
|
if errors.Is(e, storage.ErrImageUnknown) || errors.Is(e, storage.ErrLayerUnknown) {
|
|
noSuchImageErrors = true
|
|
} else if errors.Is(e, storage.ErrImageUsedByContainer) {
|
|
inUseErrors = true
|
|
} else {
|
|
otherErrors = true
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case inUseErrors:
|
|
// One of the specified images has child images or is
|
|
// being used by a container.
|
|
return 2
|
|
case noSuchImageErrors && !(otherErrors || inUseErrors):
|
|
// One of the specified images did not exist, and no other
|
|
// failures.
|
|
return 1
|
|
default:
|
|
return 125
|
|
}
|
|
}
|
|
|
|
// Remove removes one or more images from local storage.
|
|
func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entities.ImageRemoveOptions) (report *entities.ImageRemoveReport, rmErrors []error) {
|
|
report = &entities.ImageRemoveReport{}
|
|
|
|
// Set the exit code at very end.
|
|
defer func() {
|
|
report.ExitCode = removeErrorsToExitCode(rmErrors)
|
|
}()
|
|
|
|
libimageOptions := &libimage.RemoveImagesOptions{}
|
|
libimageOptions.Filters = []string{"readonly=false"}
|
|
libimageOptions.Force = opts.Force
|
|
libimageOptions.Ignore = opts.Ignore
|
|
libimageOptions.LookupManifest = opts.LookupManifest
|
|
if !opts.All {
|
|
libimageOptions.Filters = append(libimageOptions.Filters, "intermediate=false")
|
|
}
|
|
libimageOptions.RemoveContainerFunc = ir.Libpod.RemoveContainersForImageCallback(ctx)
|
|
|
|
libimageReport, libimageErrors := ir.Libpod.LibimageRuntime().RemoveImages(ctx, images, libimageOptions)
|
|
|
|
for _, r := range libimageReport {
|
|
if r.Removed {
|
|
report.Deleted = append(report.Deleted, r.ID)
|
|
}
|
|
report.Untagged = append(report.Untagged, r.Untagged...)
|
|
}
|
|
|
|
rmErrors = libimageErrors
|
|
|
|
return
|
|
}
|
|
|
|
// Shutdown Libpod engine
|
|
func (ir *ImageEngine) Shutdown(_ context.Context) {
|
|
shutdownSync.Do(func() {
|
|
_ = ir.Libpod.Shutdown(false)
|
|
})
|
|
}
|
|
|
|
func (ir *ImageEngine) Sign(ctx context.Context, names []string, options entities.SignOptions) (*entities.SignReport, error) {
|
|
mech, err := signature.NewGPGSigningMechanism()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error initializing GPG: %w", err)
|
|
}
|
|
defer mech.Close()
|
|
if err := mech.SupportsSigning(); err != nil {
|
|
return nil, fmt.Errorf("signing is not supported: %w", err)
|
|
}
|
|
sc := ir.Libpod.SystemContext()
|
|
sc.DockerCertPath = options.CertDir
|
|
sc.AuthFilePath = options.Authfile
|
|
|
|
for _, signimage := range names {
|
|
err = func() error {
|
|
srcRef, err := alltransports.ParseImageName(signimage)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing image name: %w", err)
|
|
}
|
|
rawSource, err := srcRef.NewImageSource(ctx, sc)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting image source: %w", err)
|
|
}
|
|
defer func() {
|
|
if err = rawSource.Close(); err != nil {
|
|
logrus.Errorf("Unable to close %s image source %q", srcRef.DockerReference().Name(), err)
|
|
}
|
|
}()
|
|
topManifestBlob, manifestType, err := rawSource.GetManifest(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting manifest blob: %w", err)
|
|
}
|
|
dockerReference := rawSource.Reference().DockerReference()
|
|
if dockerReference == nil {
|
|
return fmt.Errorf("cannot determine canonical Docker reference for destination %s", transports.ImageName(rawSource.Reference()))
|
|
}
|
|
var sigStoreDir string
|
|
if options.Directory != "" {
|
|
repo := reference.Path(dockerReference)
|
|
if path.Clean(repo) != repo { // Coverage: This should not be reachable because /./ and /../ components are not valid in docker references
|
|
return fmt.Errorf("unexpected path elements in Docker reference %s for signature storage", dockerReference.String())
|
|
}
|
|
sigStoreDir = filepath.Join(options.Directory, repo)
|
|
} else {
|
|
signatureURL, err := docker.SignatureStorageBaseURL(sc, rawSource.Reference(), true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sigStoreDir, err = localPathFromURI(signatureURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
manifestDigest, err := manifest.Digest(topManifestBlob)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if options.All {
|
|
if !manifest.MIMETypeIsMultiImage(manifestType) {
|
|
return fmt.Errorf("%s is not a multi-architecture image (manifest type %s)", signimage, manifestType)
|
|
}
|
|
list, err := manifest.ListFromBlob(topManifestBlob, manifestType)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing manifest list %q: %w", string(topManifestBlob), err)
|
|
}
|
|
instanceDigests := list.Instances()
|
|
for _, instanceDigest := range instanceDigests {
|
|
digest := instanceDigest
|
|
man, _, err := rawSource.GetManifest(ctx, &digest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = putSignature(man, mech, sigStoreDir, instanceDigest, dockerReference, options); err != nil {
|
|
return fmt.Errorf("error storing signature for %s, %v: %w", dockerReference.String(), instanceDigest, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err = putSignature(topManifestBlob, mech, sigStoreDir, manifestDigest, dockerReference, options); err != nil {
|
|
return fmt.Errorf("error storing signature for %s, %v: %w", dockerReference.String(), manifestDigest, err)
|
|
}
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (ir *ImageEngine) Scp(ctx context.Context, src, dst string, parentFlags []string, quiet bool) error {
|
|
rep, source, dest, flags, err := domainUtils.ExecuteTransfer(src, dst, parentFlags, quiet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if (rep == nil && err == nil) && (source != nil && dest != nil) { // we need to execute the transfer
|
|
err := Transfer(ctx, *source, *dest, flags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func Transfer(ctx context.Context, source entities.ImageScpOptions, dest entities.ImageScpOptions, parentFlags []string) error {
|
|
if source.User == "" {
|
|
return fmt.Errorf("you must define a user when transferring from root to rootless storage: %w", define.ErrInvalidArg)
|
|
}
|
|
podman, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rootless.IsRootless() && (len(dest.User) == 0 || dest.User == "root") { // if we are rootless and do not have a destination user we can just use sudo
|
|
return transferRootless(source, dest, podman, parentFlags)
|
|
}
|
|
return transferRootful(source, dest, podman, parentFlags)
|
|
}
|
|
|
|
// TransferRootless creates new podman processes using exec.Command and sudo, transferring images between the given source and destination users
|
|
func transferRootless(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error {
|
|
var cmdSave *exec.Cmd
|
|
saveCommand, loadCommand := parentFlags, parentFlags
|
|
saveCommand = append(saveCommand, []string{"save"}...)
|
|
loadCommand = append(loadCommand, []string{"load"}...)
|
|
if source.Quiet {
|
|
saveCommand = append(saveCommand, "-q")
|
|
loadCommand = append(loadCommand, "-q")
|
|
}
|
|
|
|
saveCommand = append(saveCommand, []string{"--output", source.File, source.Image}...)
|
|
|
|
loadCommand = append(loadCommand, []string{"--input", dest.File}...)
|
|
|
|
if source.User == "root" {
|
|
cmdSave = exec.Command("sudo", podman)
|
|
} else {
|
|
cmdSave = exec.Command(podman)
|
|
}
|
|
cmdSave = domainUtils.CreateSCPCommand(cmdSave, saveCommand)
|
|
logrus.Debugf("Executing save command: %q", cmdSave)
|
|
err := cmdSave.Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var cmdLoad *exec.Cmd
|
|
if source.User != "root" {
|
|
cmdLoad = exec.Command("sudo", podman)
|
|
} else {
|
|
cmdLoad = exec.Command(podman)
|
|
}
|
|
cmdLoad = domainUtils.CreateSCPCommand(cmdLoad, loadCommand)
|
|
logrus.Debugf("Executing load command: %q", cmdLoad)
|
|
if len(dest.Tag) > 0 {
|
|
return domainUtils.ScpTag(cmdLoad, podman, dest)
|
|
}
|
|
return cmdLoad.Run()
|
|
}
|
|
|
|
// transferRootful creates new podman processes using exec.Command and a new uid/gid alongside a cleared environment
|
|
func transferRootful(source entities.ImageScpOptions, dest entities.ImageScpOptions, podman string, parentFlags []string) error {
|
|
basicCommand := make([]string, 0, len(parentFlags)+1)
|
|
basicCommand = append(basicCommand, podman)
|
|
basicCommand = append(basicCommand, parentFlags...)
|
|
|
|
saveCommand := make([]string, 0, len(basicCommand)+4)
|
|
saveCommand = append(saveCommand, basicCommand...)
|
|
saveCommand = append(saveCommand, "save")
|
|
|
|
loadCommand := make([]string, 0, len(basicCommand)+3)
|
|
loadCommand = append(loadCommand, basicCommand...)
|
|
loadCommand = append(loadCommand, "load")
|
|
if source.Quiet {
|
|
saveCommand = append(saveCommand, "-q")
|
|
loadCommand = append(loadCommand, "-q")
|
|
}
|
|
saveCommand = append(saveCommand, []string{"--output", source.File, source.Image}...)
|
|
loadCommand = append(loadCommand, []string{"--input", dest.File}...)
|
|
|
|
// if executing using sudo or transferring between two users, the TransferRootless approach will not work, the new process needs to be set up
|
|
// with the proper uid and gid as well as environmental variables.
|
|
var uSave *user.User
|
|
var uLoad *user.User
|
|
var err error
|
|
source.User = strings.Split(source.User, ":")[0] // split in case provided with uid:gid
|
|
dest.User = strings.Split(dest.User, ":")[0]
|
|
uSave, err = lookupUser(source.User)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch {
|
|
case dest.User != "": // if we are given a destination user, check that first
|
|
uLoad, err = lookupUser(dest.User)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case uSave.Name != "root": // else if we have no destination user, and source is not root that means we should be root
|
|
uLoad, err = user.LookupId("0")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default: // else if we have no dest user, and source user IS root, we want to be the default user.
|
|
uString := os.Getenv("SUDO_USER")
|
|
if uString == "" {
|
|
return errors.New("$SUDO_USER must be defined to find the default rootless user")
|
|
}
|
|
uLoad, err = user.Lookup(uString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err = execTransferPodman(uSave, saveCommand, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := execTransferPodman(uLoad, loadCommand, (len(dest.Tag) > 0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out != nil {
|
|
image := domainUtils.ExtractImage(out)
|
|
_, err := execTransferPodman(uLoad, []string{podman, "tag", image, dest.Tag}, false)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func lookupUser(u string) (*user.User, error) {
|
|
if u, err := user.LookupId(u); err == nil {
|
|
return u, nil
|
|
}
|
|
return user.Lookup(u)
|
|
}
|
|
|
|
func execTransferPodman(execUser *user.User, command []string, needToTag bool) ([]byte, error) {
|
|
cmdLogin, err := domainUtils.LoginUser(execUser.Username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer func() {
|
|
_ = cmdLogin.Process.Kill()
|
|
_ = cmdLogin.Wait()
|
|
}()
|
|
|
|
cmd := exec.Command(command[0], command[1:]...)
|
|
cmd.Env = []string{"PATH=" + os.Getenv("PATH"), "TERM=" + os.Getenv("TERM")}
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdout = os.Stdout
|
|
uid, err := strconv.ParseInt(execUser.Uid, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gid, err := strconv.ParseInt(execUser.Gid, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Credential: &syscall.Credential{
|
|
Uid: uint32(uid),
|
|
Gid: uint32(gid),
|
|
Groups: nil,
|
|
NoSetGroups: false,
|
|
},
|
|
}
|
|
if needToTag {
|
|
cmd.Stdout = nil
|
|
return cmd.Output()
|
|
}
|
|
return nil, cmd.Run()
|
|
}
|
|
|
|
func getSigFilename(sigStoreDirPath string) (string, error) {
|
|
sigFileSuffix := 1
|
|
sigFiles, err := ioutil.ReadDir(sigStoreDirPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sigFilenames := make(map[string]bool)
|
|
for _, file := range sigFiles {
|
|
sigFilenames[file.Name()] = true
|
|
}
|
|
for {
|
|
sigFilename := "signature-" + strconv.Itoa(sigFileSuffix)
|
|
if _, exists := sigFilenames[sigFilename]; !exists {
|
|
return sigFilename, nil
|
|
}
|
|
sigFileSuffix++
|
|
}
|
|
}
|
|
|
|
func localPathFromURI(url *url.URL) (string, error) {
|
|
if url.Scheme != "file" {
|
|
return "", fmt.Errorf("writing to %s is not supported. Use a supported scheme", url.String())
|
|
}
|
|
return url.Path, nil
|
|
}
|
|
|
|
// putSignature creates signature and saves it to the signstore file
|
|
func putSignature(manifestBlob []byte, mech signature.SigningMechanism, sigStoreDir string, instanceDigest digest.Digest, dockerReference dockerRef.Reference, options entities.SignOptions) error {
|
|
newSig, err := signature.SignDockerManifest(manifestBlob, dockerReference.String(), mech, options.SignBy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
signatureDir := fmt.Sprintf("%s@%s=%s", sigStoreDir, instanceDigest.Algorithm(), instanceDigest.Hex())
|
|
if err := os.MkdirAll(signatureDir, 0751); err != nil {
|
|
// The directory is allowed to exist
|
|
if !errors.Is(err, fs.ErrExist) {
|
|
return err
|
|
}
|
|
}
|
|
sigFilename, err := getSigFilename(signatureDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutil.WriteFile(filepath.Join(signatureDir, sigFilename), newSig, 0644)
|
|
}
|