mirror of
https://github.com/containers/podman.git
synced 2025-06-24 11:28:24 +08:00
Consume OCI images for machine image
allow podman machine to extract its disk image from an oci registry or oci-dir locally. for now, the image must be relatively inflexible. it must have 1 layer. the layer must possess one image. so a dockerfile like: FROM scratch COPY ./myimage.xz /myimage.xz when using an oci dir, the directory structure must adhere to the typical directory structure of a an oci image (with one layer). ── blobs │ └── sha256 │ ├── 53735773573b3853bb1cae16dd21061beb416239ceb78d4ef1f2a0609f7e843b │ ├── 80577866ec13c041693e17de61444b4696137623803c3d87f92e4f28a1f4e87b │ └── af57637ac1ab12f833e3cfa886027cc9834a755a437d0e1cf48b5d4778af7a4e ├── index.json └── oci-layout in order to identify this new input, you must use a transport/schema to differentiate from current podman machine init --image-path behavior. we will support `oci-dir://` and `docker://` as transports. when using the docker transport, you can only use an empty transport for input. for example, `podman machine init --image-path docker://`. A fully quailified image name will be supported in the next iteration. the transport absent anything means, i want to pull the default fcos image stored in a registry. podman will determine its current version and then look for its correlating manifest. in this default use case, it would look for: quay.io/libpod/podman-machine-images:<version> that manifest would then point to specific images that contain the correct arch and provider disk image. i.e. quay.io/libpod/podman-machine-images:4.6-qcow2 this PR does not enable something like docker://quay.io/mycorp/myimage:latest yet. names, addresses, andf schema/transports are all subject to change. the plan is to keep this all undocumented until things firm up. [NO NEW TESTS NEEDED] Signed-off-by: Brent Baude <bbaude@redhat.com>
This commit is contained in:
@ -38,6 +38,7 @@ func stop(cmd *cobra.Command, args []string) error {
|
|||||||
err error
|
err error
|
||||||
vm machine.VM
|
vm machine.VM
|
||||||
)
|
)
|
||||||
|
|
||||||
vmName := defaultMachineName
|
vmName := defaultMachineName
|
||||||
if len(args) > 0 && len(args[0]) > 0 {
|
if len(args) > 0 && len(args[0]) > 0 {
|
||||||
vmName = args[0]
|
vmName = args[0]
|
||||||
|
@ -585,6 +585,7 @@ func (dl Download) AcquireVMImage(imagePath string) (*VMFile, FCOSStream, error)
|
|||||||
imageLocation *VMFile
|
imageLocation *VMFile
|
||||||
fcosStream FCOSStream
|
fcosStream FCOSStream
|
||||||
)
|
)
|
||||||
|
|
||||||
switch imagePath {
|
switch imagePath {
|
||||||
// TODO these need to be re-typed as FCOSStreams
|
// TODO these need to be re-typed as FCOSStreams
|
||||||
case Testing.String(), Next.String(), Stable.String(), "":
|
case Testing.String(), Next.String(), Stable.String(), "":
|
||||||
|
154
pkg/machine/default.go
Normal file
154
pkg/machine/default.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/types"
|
||||||
|
"github.com/containers/podman/v4/pkg/machine/ocipull"
|
||||||
|
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Versioned struct {
|
||||||
|
blob *types.BlobInfo
|
||||||
|
blobDirPath string
|
||||||
|
cacheDir string
|
||||||
|
ctx context.Context
|
||||||
|
imageFormat ImageFormat
|
||||||
|
imageName string
|
||||||
|
machineImageDir string
|
||||||
|
machineVersion *OSVersion
|
||||||
|
vmName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVersioned(ctx context.Context, machineImageDir, vmName string) (*Versioned, error) {
|
||||||
|
imageCacheDir := filepath.Join(machineImageDir, "cache")
|
||||||
|
if err := os.MkdirAll(imageCacheDir, 0777); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
o := getVersion()
|
||||||
|
return &Versioned{ctx: ctx, cacheDir: imageCacheDir, machineImageDir: machineImageDir, machineVersion: o, vmName: vmName}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) LocalBlob() *types.BlobInfo {
|
||||||
|
return d.blob
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) DiskEndpoint() string {
|
||||||
|
return d.machineVersion.diskImage(d.imageFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) versionedOCICacheDir() string {
|
||||||
|
return filepath.Join(d.cacheDir, d.machineVersion.majorMinor())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) identifyImageNameFromOCIDir() (string, error) {
|
||||||
|
imageManifest, err := ocipull.ReadImageManifestFromOCIPath(d.ctx, d.versionedOCICacheDir())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(imageManifest.Layers) > 1 {
|
||||||
|
return "", fmt.Errorf("podman machine images can have only one layer: %d found", len(imageManifest.Layers))
|
||||||
|
}
|
||||||
|
path := filepath.Join(d.versionedOCICacheDir(), "blobs", "sha256", imageManifest.Layers[0].Digest.Hex())
|
||||||
|
return findTarComponent(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) pull(path string) error {
|
||||||
|
fmt.Printf("Pulling %s\n", d.DiskEndpoint())
|
||||||
|
logrus.Debugf("pulling %s to %s", d.DiskEndpoint(), path)
|
||||||
|
return ocipull.Pull(d.ctx, d.DiskEndpoint(), path, ocipull.PullOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) Pull() error {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
isUpdatable bool
|
||||||
|
localBlob *types.BlobInfo
|
||||||
|
remoteDescriptor *v1.Descriptor
|
||||||
|
)
|
||||||
|
|
||||||
|
remoteDiskImage := d.machineVersion.diskImage(Qcow)
|
||||||
|
logrus.Debugf("podman disk image name: %s", remoteDiskImage)
|
||||||
|
|
||||||
|
// is there a valid oci dir in our cache
|
||||||
|
hasCache := d.localOCIDirExists()
|
||||||
|
|
||||||
|
if hasCache {
|
||||||
|
logrus.Debug("checking remote registry")
|
||||||
|
remoteDescriptor, err = ocipull.GetRemoteDescriptor(d.ctx, remoteDiskImage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Debugf("working with local cache: %s", d.versionedOCICacheDir())
|
||||||
|
localBlob, err = ocipull.GetLocalBlob(d.ctx, d.versionedOCICacheDir())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// determine if the local is same as remote
|
||||||
|
if remoteDescriptor.Digest.Hex() != localBlob.Digest.Hex() {
|
||||||
|
logrus.Debugf("new image is available: %s", remoteDescriptor.Digest.Hex())
|
||||||
|
isUpdatable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasCache || isUpdatable {
|
||||||
|
if hasCache {
|
||||||
|
if err := GuardedRemoveAll(d.versionedOCICacheDir()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := d.pull(d.versionedOCICacheDir()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageName, err := d.identifyImageNameFromOCIDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logrus.Debugf("image name: %s", imageName)
|
||||||
|
d.imageName = imageName
|
||||||
|
|
||||||
|
if localBlob == nil {
|
||||||
|
localBlob, err = ocipull.GetLocalBlob(d.ctx, d.versionedOCICacheDir())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.blob = localBlob
|
||||||
|
d.blobDirPath = d.versionedOCICacheDir()
|
||||||
|
logrus.Debugf("local oci disk image blob: %s", d.localOCIDiskImageDir(localBlob))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) Unpack() (*VMFile, error) {
|
||||||
|
tbPath := localOCIDiskImageDir(d.blobDirPath, d.blob)
|
||||||
|
unpackedFile, err := unpackOCIDir(tbPath, d.machineImageDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d.imageName = unpackedFile.GetPath()
|
||||||
|
return unpackedFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) Decompress(compressedFile *VMFile) (*VMFile, error) {
|
||||||
|
imageCompression := compressionFromFile(d.imageName)
|
||||||
|
strippedImageName := strings.TrimSuffix(d.imageName, fmt.Sprintf(".%s", imageCompression.String()))
|
||||||
|
finalName := finalFQImagePathName(d.vmName, strippedImageName)
|
||||||
|
if err := Decompress(compressedFile, finalName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewMachineFile(finalName, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) localOCIDiskImageDir(localBlob *types.BlobInfo) string {
|
||||||
|
return filepath.Join(d.versionedOCICacheDir(), "blobs", "sha256", localBlob.Digest.Hex())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Versioned) localOCIDirExists() bool {
|
||||||
|
_, indexErr := os.Stat(filepath.Join(d.versionedOCICacheDir(), "index.json"))
|
||||||
|
return indexErr == nil
|
||||||
|
}
|
@ -77,7 +77,11 @@ var _ = BeforeSuite(func() {
|
|||||||
Fail(fmt.Sprintf("unable to download machine image: %q", err))
|
Fail(fmt.Sprintf("unable to download machine image: %q", err))
|
||||||
}
|
}
|
||||||
GinkgoWriter.Println("Download took: ", time.Since(now).String())
|
GinkgoWriter.Println("Download took: ", time.Since(now).String())
|
||||||
if err := machine.Decompress(fqImageName+compressionExtension, fqImageName); err != nil {
|
diskImage, err := machine.NewMachineFile(fqImageName+compressionExtension, nil)
|
||||||
|
if err != nil {
|
||||||
|
Fail(fmt.Sprintf("unable to create vmfile %q: %v", fqImageName+compressionExtension, err))
|
||||||
|
}
|
||||||
|
if err := machine.Decompress(diskImage, fqImageName); err != nil {
|
||||||
Fail(fmt.Sprintf("unable to decompress image file: %q", err))
|
Fail(fmt.Sprintf("unable to decompress image file: %q", err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -78,6 +78,18 @@ func (imf ImageFormat) String() string {
|
|||||||
return "qcow2.xz"
|
return "qcow2.xz"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (imf ImageFormat) string() string {
|
||||||
|
switch imf {
|
||||||
|
case Vhdx:
|
||||||
|
return "vhdx"
|
||||||
|
case Tar:
|
||||||
|
return "tar"
|
||||||
|
case Raw:
|
||||||
|
return "raw"
|
||||||
|
}
|
||||||
|
return "qcow2"
|
||||||
|
}
|
||||||
|
|
||||||
func (c ImageCompression) String() string {
|
func (c ImageCompression) String() string {
|
||||||
switch c {
|
switch c {
|
||||||
case Gz:
|
case Gz:
|
||||||
|
136
pkg/machine/oci.go
Normal file
136
pkg/machine/oci.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/types"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/pkg/compression"
|
||||||
|
"github.com/containers/storage/pkg/archive"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/blang/semver/v4"
|
||||||
|
"github.com/containers/podman/v4/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// quay.io/libpod/podman-machine-images:4.6
|
||||||
|
|
||||||
|
const (
|
||||||
|
diskImages = "podman-machine-images"
|
||||||
|
registry = "quay.io"
|
||||||
|
repo = "libpod"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OSVersion struct {
|
||||||
|
*semver.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
type Disker interface {
|
||||||
|
Pull() error
|
||||||
|
Decompress(compressedFile *VMFile) (*VMFile, error)
|
||||||
|
DiskEndpoint() string
|
||||||
|
Unpack() (*VMFile, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OCIOpts struct {
|
||||||
|
Scheme *OCIKind
|
||||||
|
Dir *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OCIKind string
|
||||||
|
|
||||||
|
var (
|
||||||
|
OCIDir OCIKind = "oci-dir"
|
||||||
|
OCIRegistry OCIKind = "docker"
|
||||||
|
OCIUnknown OCIKind = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (o OCIKind) String() string {
|
||||||
|
switch o {
|
||||||
|
case OCIDir:
|
||||||
|
return string(OCIDir)
|
||||||
|
case OCIRegistry:
|
||||||
|
return string(OCIRegistry)
|
||||||
|
}
|
||||||
|
return string(OCIUnknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o OCIKind) IsOCIDir() bool {
|
||||||
|
return o == OCIDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func StripOCIReference(input string) string {
|
||||||
|
return strings.TrimPrefix(input, "docker://")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVersion() *OSVersion {
|
||||||
|
v := version.Version
|
||||||
|
|
||||||
|
// OVERRIDES FOR DEV ONLY
|
||||||
|
v.Minor = 6
|
||||||
|
v.Pre = nil
|
||||||
|
// OVERRIDES FOR DEV ONLY
|
||||||
|
|
||||||
|
return &OSVersion{&v}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OSVersion) majorMinor() string {
|
||||||
|
return fmt.Sprintf("%d.%d", o.Major, o.Minor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OSVersion) diskImage(diskFlavor ImageFormat) string {
|
||||||
|
return fmt.Sprintf("%s/%s/%s:%s-%s", registry, repo, diskImages, o.majorMinor(), diskFlavor.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackOCIDir(ociTb, machineImageDir string) (*VMFile, error) {
|
||||||
|
imageFileName, err := findTarComponent(ociTb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unpackedFileName := filepath.Join(machineImageDir, imageFileName)
|
||||||
|
|
||||||
|
f, err := os.Open(ociTb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
logrus.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
uncompressedReader, _, err := compression.AutoDecompress(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := uncompressedReader.Close(); err != nil {
|
||||||
|
logrus.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logrus.Debugf("untarring %q to %q", ociTb, machineImageDir)
|
||||||
|
if err := archive.Untar(uncompressedReader, machineImageDir, &archive.TarOptions{
|
||||||
|
NoLchown: true,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewMachineFile(unpackedFileName, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func localOCIDiskImageDir(blobDirPath string, localBlob *types.BlobInfo) string {
|
||||||
|
return filepath.Join(blobDirPath, "blobs", "sha256", localBlob.Digest.Hex())
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalFQImagePathName(vmName, imageName string) string {
|
||||||
|
// imageName here is fully qualified. we need to break
|
||||||
|
// it apart and add the vmname
|
||||||
|
baseDir, filename := filepath.Split(imageName)
|
||||||
|
return filepath.Join(baseDir, fmt.Sprintf("%s-%s", vmName, filename))
|
||||||
|
}
|
116
pkg/machine/ocidir.go
Normal file
116
pkg/machine/ocidir.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/pkg/compression"
|
||||||
|
"github.com/containers/image/v5/types"
|
||||||
|
"github.com/containers/podman/v4/pkg/machine/ocipull"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalBlobDir struct {
|
||||||
|
blob *types.BlobInfo
|
||||||
|
blobDirPath string
|
||||||
|
ctx context.Context
|
||||||
|
imageName string
|
||||||
|
machineImageDir string
|
||||||
|
vmName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOCIDir(ctx context.Context, inputDir, machineImageDir, vmName string) *LocalBlobDir {
|
||||||
|
strippedInputDir := strings.TrimPrefix(inputDir, fmt.Sprintf("%s:/", OCIDir.String()))
|
||||||
|
l := LocalBlobDir{
|
||||||
|
blob: nil,
|
||||||
|
blobDirPath: strippedInputDir,
|
||||||
|
ctx: ctx,
|
||||||
|
imageName: "",
|
||||||
|
machineImageDir: machineImageDir,
|
||||||
|
vmName: vmName,
|
||||||
|
}
|
||||||
|
return &l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalBlobDir) Pull() error {
|
||||||
|
localBlob, err := ocipull.GetLocalBlob(l.ctx, l.DiskEndpoint())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l.blob = localBlob
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalBlobDir) Decompress(compressedFile *VMFile) (*VMFile, error) {
|
||||||
|
finalName := finalFQImagePathName(l.vmName, l.imageName)
|
||||||
|
if err := Decompress(compressedFile, finalName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewMachineFile(finalName, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalBlobDir) Unpack() (*VMFile, error) {
|
||||||
|
tbPath := localOCIDiskImageDir(l.blobDirPath, l.blob)
|
||||||
|
unPackedFile, err := unpackOCIDir(tbPath, l.machineImageDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
l.imageName = unPackedFile.GetPath()
|
||||||
|
return unPackedFile, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalBlobDir) DiskEndpoint() string {
|
||||||
|
return l.blobDirPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalBlobDir) LocalBlob() *types.BlobInfo {
|
||||||
|
return l.blob
|
||||||
|
}
|
||||||
|
|
||||||
|
// findTarComponent returns a header and a reader matching componentPath within inputFile,
|
||||||
|
// or (nil, nil, nil) if not found.
|
||||||
|
func findTarComponent(pathToTar string) (string, error) {
|
||||||
|
f, err := os.Open(pathToTar)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
logrus.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
uncompressedReader, _, err := compression.AutoDecompress(f)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := uncompressedReader.Close(); err != nil {
|
||||||
|
logrus.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var (
|
||||||
|
filename string
|
||||||
|
headerCount uint
|
||||||
|
)
|
||||||
|
t := tar.NewReader(uncompressedReader)
|
||||||
|
for {
|
||||||
|
h, err := t.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
filename = h.Name
|
||||||
|
headerCount++
|
||||||
|
}
|
||||||
|
if headerCount != 1 {
|
||||||
|
return "", errors.New("invalid oci machine image")
|
||||||
|
}
|
||||||
|
return filename, nil
|
||||||
|
}
|
110
pkg/machine/ocipull/pull.go
Normal file
110
pkg/machine/ocipull/pull.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package ocipull
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/containers/buildah/pkg/parse"
|
||||||
|
"github.com/containers/image/v5/copy"
|
||||||
|
"github.com/containers/image/v5/oci/layout"
|
||||||
|
"github.com/containers/image/v5/pkg/shortnames"
|
||||||
|
"github.com/containers/image/v5/signature"
|
||||||
|
"github.com/containers/image/v5/transports/alltransports"
|
||||||
|
"github.com/containers/image/v5/types"
|
||||||
|
specV1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PullOptions includes data to alter certain knobs when pulling a source
|
||||||
|
// image.
|
||||||
|
type PullOptions struct {
|
||||||
|
// Require HTTPS and verify certificates when accessing the registry.
|
||||||
|
TLSVerify bool
|
||||||
|
// [username[:password] to use when connecting to the registry.
|
||||||
|
Credentials string
|
||||||
|
// Quiet the progress bars when pushing.
|
||||||
|
Quiet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull `imageInput` from a container registry to `sourcePath`.
|
||||||
|
func Pull(ctx context.Context, imageInput string, sourcePath string, options PullOptions) error {
|
||||||
|
if _, err := os.Stat(sourcePath); err == nil {
|
||||||
|
return fmt.Errorf("%q already exists", sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
srcRef, err := stringToImageReference(imageInput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
destRef, err := layout.ParseReference(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sysCtx := &types.SystemContext{
|
||||||
|
DockerInsecureSkipTLSVerify: types.NewOptionalBool(!options.TLSVerify),
|
||||||
|
}
|
||||||
|
if options.Credentials != "" {
|
||||||
|
authConf, err := parse.AuthConfig(options.Credentials)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sysCtx.DockerAuthConfig = authConf
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateSourceImageReference(ctx, srcRef, sysCtx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
policy, err := signature.DefaultPolicy(sysCtx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("obtaining default signature policy: %w", err)
|
||||||
|
}
|
||||||
|
policyContext, err := signature.NewPolicyContext(policy)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating new signature policy context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
copyOpts := copy.Options{
|
||||||
|
SourceCtx: sysCtx,
|
||||||
|
}
|
||||||
|
if !options.Quiet {
|
||||||
|
copyOpts.ReportWriter = os.Stderr
|
||||||
|
}
|
||||||
|
if _, err := copy.Image(ctx, policyContext, destRef, srcRef, ©Opts); err != nil {
|
||||||
|
return fmt.Errorf("pulling source image: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringToImageReference(imageInput string) (types.ImageReference, error) {
|
||||||
|
if shortnames.IsShortName(imageInput) {
|
||||||
|
return nil, fmt.Errorf("pulling source images by short name (%q) is not supported, please use a fully-qualified name", imageInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := alltransports.ParseImageName("docker://" + imageInput)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing image name: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateSourceImageReference(ctx context.Context, ref types.ImageReference, sysCtx *types.SystemContext) error {
|
||||||
|
src, err := ref.NewImageSource(ctx, sysCtx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating image source from reference: %w", err)
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
ociManifest, _, _, err := readManifestFromImageSource(ctx, src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ociManifest.Config.MediaType != specV1.MediaTypeImageConfig {
|
||||||
|
return fmt.Errorf("invalid media type of image config %q (expected: %q)", ociManifest.Config.MediaType, specV1.MediaTypeImageConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
120
pkg/machine/ocipull/source.go
Normal file
120
pkg/machine/ocipull/source.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package ocipull
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containers/image/v5/docker"
|
||||||
|
"github.com/containers/image/v5/oci/layout"
|
||||||
|
"github.com/containers/image/v5/types"
|
||||||
|
"github.com/opencontainers/go-digest"
|
||||||
|
specV1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readManifestFromImageSource reads the manifest from the specified image
|
||||||
|
// source. Note that the manifest is expected to be an OCI v1 manifest.
|
||||||
|
func readManifestFromImageSource(ctx context.Context, src types.ImageSource) (*specV1.Manifest, *digest.Digest, int64, error) {
|
||||||
|
rawData, mimeType, err := src.GetManifest(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, -1, err
|
||||||
|
}
|
||||||
|
if mimeType != specV1.MediaTypeImageManifest {
|
||||||
|
return nil, nil, -1, fmt.Errorf("image %q is of type %q (expected: %q)", strings.TrimPrefix(src.Reference().StringWithinTransport(), "//"), mimeType, specV1.MediaTypeImageManifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := specV1.Manifest{}
|
||||||
|
if err := json.Unmarshal(rawData, &manifest); err != nil {
|
||||||
|
return nil, nil, -1, fmt.Errorf("reading manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestDigest := digest.FromBytes(rawData)
|
||||||
|
return &manifest, &manifestDigest, int64(len(rawData)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readManifestFromOCIPath returns the manifest of the specified source image
|
||||||
|
// at `sourcePath` along with its digest. The digest can later on be used to
|
||||||
|
// locate the manifest on the file system.
|
||||||
|
func readManifestFromOCIPath(ctx context.Context, sourcePath string) (*specV1.Manifest, *digest.Digest, int64, error) {
|
||||||
|
ociRef, err := layout.ParseReference(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ociSource, err := ociRef.NewImageSource(ctx, &types.SystemContext{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, -1, err
|
||||||
|
}
|
||||||
|
defer ociSource.Close()
|
||||||
|
|
||||||
|
return readManifestFromImageSource(ctx, ociSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLocalBlob(ctx context.Context, path string) (*types.BlobInfo, error) {
|
||||||
|
ociRef, err := layout.ParseReference(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
img, err := ociRef.NewImage(ctx, &types.SystemContext{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _, err := img.Manifest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localManifest := specV1.Manifest{}
|
||||||
|
if err := json.Unmarshal(b, &localManifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
blobs := img.LayerInfos()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(blobs) != 1 {
|
||||||
|
return nil, errors.New("invalid disk image")
|
||||||
|
}
|
||||||
|
fmt.Println(blobs[0].Digest.Hex())
|
||||||
|
return &blobs[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRemoteManifest(ctx context.Context, dest string) (*specV1.Manifest, error) {
|
||||||
|
ref, err := docker.ParseReference(fmt.Sprintf("//%s", dest))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
imgSrc, err := ref.NewImage(ctx, &types.SystemContext{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _, err := imgSrc.Manifest(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteManifest := specV1.Manifest{}
|
||||||
|
err = json.Unmarshal(b, &remoteManifest)
|
||||||
|
return &remoteManifest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRemoteDescriptor(ctx context.Context, dest string) (*specV1.Descriptor, error) {
|
||||||
|
remoteManifest, err := GetRemoteManifest(ctx, dest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(remoteManifest.Layers) != 1 {
|
||||||
|
return nil, errors.New("invalid remote disk image")
|
||||||
|
}
|
||||||
|
return &remoteManifest.Layers[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadImageManifestFromOCIPath(ctx context.Context, ociImagePath string) (*specV1.Manifest, error) {
|
||||||
|
imageManifest, _, _, err := readManifestFromOCIPath(ctx, ociImagePath)
|
||||||
|
return imageManifest, err
|
||||||
|
}
|
@ -6,6 +6,7 @@ package machine
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -133,7 +134,11 @@ func DownloadImage(d DistributionDownload) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
return Decompress(d.Get().LocalPath, d.Get().LocalUncompressedFile)
|
localPath, err := NewMachineFile(d.Get().LocalPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Decompress(localPath, d.Get().LocalUncompressedFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func progressBar(prefix string, size int64, onComplete string) (*mpb.Progress, *mpb.Bar) {
|
func progressBar(prefix string, size int64, onComplete string) (*mpb.Progress, *mpb.Bar) {
|
||||||
@ -205,17 +210,17 @@ func DownloadVMImage(downloadURL *url2.URL, imageName string, localImagePath str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Decompress(localPath, uncompressedPath string) error {
|
func Decompress(localPath *VMFile, uncompressedPath string) error {
|
||||||
var isZip bool
|
var isZip bool
|
||||||
uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600)
|
uncompressedFileWriter, err := os.OpenFile(uncompressedPath, os.O_CREATE|os.O_RDWR, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sourceFile, err := os.ReadFile(localPath)
|
sourceFile, err := localPath.Read()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(localPath, ".zip") {
|
if strings.HasSuffix(localPath.GetPath(), ".zip") {
|
||||||
isZip = true
|
isZip = true
|
||||||
}
|
}
|
||||||
prefix := "Copying uncompressed file"
|
prefix := "Copying uncompressed file"
|
||||||
@ -225,12 +230,12 @@ func Decompress(localPath, uncompressedPath string) error {
|
|||||||
}
|
}
|
||||||
prefix += ": " + filepath.Base(uncompressedPath)
|
prefix += ": " + filepath.Base(uncompressedPath)
|
||||||
if compressionType == archive.Xz {
|
if compressionType == archive.Xz {
|
||||||
return decompressXZ(prefix, localPath, uncompressedFileWriter)
|
return decompressXZ(prefix, localPath.GetPath(), uncompressedFileWriter)
|
||||||
}
|
}
|
||||||
if isZip && runtime.GOOS == "windows" {
|
if isZip && runtime.GOOS == "windows" {
|
||||||
return decompressZip(prefix, localPath, uncompressedFileWriter)
|
return decompressZip(prefix, localPath.GetPath(), uncompressedFileWriter)
|
||||||
}
|
}
|
||||||
return decompressEverythingElse(prefix, localPath, uncompressedFileWriter)
|
return decompressEverythingElse(prefix, localPath.GetPath(), uncompressedFileWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Will error out if file without .Xz already exists
|
// Will error out if file without .Xz already exists
|
||||||
@ -405,3 +410,75 @@ func (dl Download) AcquireAlternateImage(inputPath string) (*VMFile, error) {
|
|||||||
|
|
||||||
return imagePath, nil
|
return imagePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isOci(input string) (bool, *OCIKind, error) {
|
||||||
|
inputURL, err := url2.Parse(input)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
switch inputURL.Scheme {
|
||||||
|
case OCIDir.String():
|
||||||
|
return true, &OCIDir, nil
|
||||||
|
case OCIRegistry.String():
|
||||||
|
return true, &OCIRegistry, nil
|
||||||
|
}
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pull(input, machineName string, vp VirtProvider) (*VMFile, FCOSStream, error) {
|
||||||
|
var (
|
||||||
|
disk Disker
|
||||||
|
)
|
||||||
|
|
||||||
|
ociBased, ociScheme, err := isOci(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if !ociBased {
|
||||||
|
// Business as usual
|
||||||
|
dl, err := vp.NewDownload(machineName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return dl.AcquireVMImage(input)
|
||||||
|
}
|
||||||
|
oopts := OCIOpts{
|
||||||
|
Scheme: ociScheme,
|
||||||
|
}
|
||||||
|
dataDir, err := GetDataDir(vp.VMType())
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if ociScheme.IsOCIDir() {
|
||||||
|
strippedOCIDir := StripOCIReference(input)
|
||||||
|
oopts.Dir = &strippedOCIDir
|
||||||
|
disk = NewOCIDir(context.Background(), input, dataDir, machineName)
|
||||||
|
} else {
|
||||||
|
// a use of a containers image type here might be
|
||||||
|
// tighter
|
||||||
|
strippedInput := strings.TrimPrefix(input, "docker://")
|
||||||
|
// this is the next piece of work
|
||||||
|
if len(strippedInput) > 0 {
|
||||||
|
return nil, 0, errors.New("image names are not supported yet")
|
||||||
|
}
|
||||||
|
disk, err = newVersioned(context.Background(), dataDir, machineName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := disk.Pull(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
unpacked, err := disk.Unpack()
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
logrus.Debugf("cleaning up %q", unpacked.GetPath())
|
||||||
|
if err := unpacked.Delete(); err != nil {
|
||||||
|
logrus.Errorf("unable to delete local compressed file %q:%v", unpacked.GetPath(), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
imagePath, err := disk.Decompress(unpacked)
|
||||||
|
return imagePath, UnknownStream, err
|
||||||
|
}
|
||||||
|
@ -260,15 +260,12 @@ func (v *MachineVM) Init(opts machine.InitOptions) (bool, error) {
|
|||||||
v.IdentityPath = util.GetIdentityPath(v.Name)
|
v.IdentityPath = util.GetIdentityPath(v.Name)
|
||||||
v.Rootful = opts.Rootful
|
v.Rootful = opts.Rootful
|
||||||
|
|
||||||
dl, err := VirtualizationProvider().NewDownload(v.Name)
|
imagePath, strm, err := machine.Pull(opts.ImagePath, opts.Name, VirtualizationProvider())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePath, strm, err := dl.AcquireVMImage(opts.ImagePath)
|
// By this time, image should be had and uncompressed
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
callbackFuncs.Add(imagePath.Delete)
|
callbackFuncs.Add(imagePath.Delete)
|
||||||
|
|
||||||
// Assign values about the download
|
// Assign values about the download
|
||||||
|
Reference in New Issue
Block a user