working image push/pull

This commit is contained in:
Quentin 2023-05-03 09:52:46 +02:00
parent 9b10486aa4
commit 29b6a209a4
Signed by: quentin
GPG key ID: E9602264D639FF68
2 changed files with 327 additions and 31 deletions

View file

@ -1,18 +1,24 @@
package cmd package cmd
import ( import (
"crypto/sha256"
"errors" "errors"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io/ioutil" "io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/signature" "github.com/containers/image/v5/signature"
"github.com/containers/image/v5/copy" "github.com/containers/image/v5/copy"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gocloud.dev/blob"
_ "gocloud.dev/blob/s3blob"
) )
var pctx *signature.PolicyContext var pctx *signature.PolicyContext
@ -21,10 +27,39 @@ const distributionPrefix = "v2"
//--- //---
//--- Image converter //--- Image converter
type OCIPlatform struct { type OCIImageManifest struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
Config OCIRef `json:"config"`
Layers []OCIRef `json:layers"`
}
func LoadOCIImageManifest(path string) (*OCIImageManifest, error) {
fd, err := os.Open(path)
if err != nil {
return nil, err
}
defer fd.Close()
b, err := io.ReadAll(fd)
if err != nil {
return nil, err
}
oim := &OCIImageManifest{}
err = json.Unmarshal(b, oim)
if err != nil {
return nil, err
}
return oim, nil
} }
type OCIImageManifest struct { type OCIPlatform struct {
Architecture string `json:"architecture"`
OS string `json:"os"`
}
type OCIRef struct {
MediaType string `json:"mediaType"` MediaType string `json:"mediaType"`
Digest string `json:"digest"` Digest string `json:"digest"`
Size int `json:"size"` Size int `json:"size"`
@ -34,10 +69,65 @@ type OCIImageManifest struct {
type OCIImageIndex struct { type OCIImageIndex struct {
SchemaVersion int `json:"schemaVersion"` SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"` MediaType string `json:"mediaType"`
Manifests []OCIImageManifest `json:"manifests"` Manifests []OCIRef `json:"manifests"`
}
func NewOCIImageIndex(o *OCIMultiArch) (OCIImageIndex, error) {
idx := OCIImageIndex {
SchemaVersion: 2,
MediaType: "application/vnd.oci.image.index.v1+json",
Manifests: []OCIRef{},
}
for _, syst := range o.images {
if syst.index == nil {
return idx, errors.New(fmt.Sprintf("Missing index content for %s. Check that the file exists and that it can be parsed", syst.path))
}
if len(syst.index.Manifests) != 1 {
return idx, errors.New(fmt.Sprintf("%s is not a system image as it does not contain exactly one manifest in its index", syst.path))
}
if syst.index.Manifests[0].Platform == nil {
return idx, errors.New(fmt.Sprintf("Manifest for %s has not been enriched with its platform (os+arch). This is a logic bug.", syst.path))
}
idx.Manifests = append(idx.Manifests, syst.index.Manifests[0])
}
return idx, nil
} }
type OCILayout { func LoadOCIImageIndex(path string) (*OCIImageIndex, error) {
fd, err := os.Open(filepath.Join(path, "index.json"))
if err != nil {
return nil, err
}
defer fd.Close()
b, err := io.ReadAll(fd)
if err != nil {
return nil, err
}
var idx OCIImageIndex
err = json.Unmarshal(b, &idx)
if err != nil {
return nil, err
}
return &idx, nil
}
func (i OCIImageIndex) WriteTo(root string) error {
txt, err := json.Marshal(i)
if err != nil {
return err
}
txtPath := filepath.Join(root, "index.json")
err = os.WriteFile(txtPath, txt, 0644)
return err
}
type OCILayout struct {
ImageLayoutVersion string `json:"imageLayoutVersion"` ImageLayoutVersion string `json:"imageLayoutVersion"`
} }
func NewOCILayout() OCILayout { func NewOCILayout() OCILayout {
@ -50,23 +140,53 @@ func (o OCILayout) WriteTo(root string) error {
} }
txtPath := filepath.Join(root, "oci-layout") txtPath := filepath.Join(root, "oci-layout")
err = ioutil.WriteFile(txtPath, txt, 0644) err = os.WriteFile(txtPath, txt, 0644)
return err return err
} }
//---- Alba logic
type OCISystemImage struct { type OCISystemImage struct {
path string path string
os string os string
arch string arch string
index *OCIImageIndex
} }
func NewOCISystemImage(path, os, arch string) (OCISystemImage, error) {
si := OCISystemImage {
path: path,
os: os,
arch: arch,
index: nil,
}
man, err := LoadOCIImageIndex(path)
if err != nil {
return si, err
}
si.index = man
if len(si.index.Manifests) != 1 {
return si, errors.New(fmt.Sprintf("%s index has not exactly one manifest, this can't be a system image\n", path))
}
// Enrich manifest
si.index.Manifests[0].Platform = &OCIPlatform {
Architecture: arch,
OS: os,
}
return si, nil
}
func (o OCISystemImage) String() string { func (o OCISystemImage) String() string {
return fmt.Sprintf("[oci system image; os:%s, arch:%s, path:%s]", o.os, o.arch, o.path) return fmt.Sprintf("[oci system image; os:%s, arch:%s, path:%s]", o.os, o.arch, o.path)
} }
type OCIMultiArch struct { type OCIMultiArch struct {
name string Name string
tag string Tag string
path string path string
multi *OCIImageIndex
images []OCISystemImage images []OCISystemImage
} }
@ -82,8 +202,8 @@ func NewOCIMultiArch(nametag string) (*OCIMultiArch, error) {
} }
return &OCIMultiArch { return &OCIMultiArch {
name: ntspl[0], Name: ntspl[0],
tag: ntspl[1], Tag: ntspl[1],
path: tmp, path: tmp,
images: []OCISystemImage{}, images: []OCISystemImage{},
}, nil }, nil
@ -100,6 +220,7 @@ func (o *OCIMultiArch) LoadFromDockerArchives(path string) error {
return err return err
} }
// Convert all docker archives to oci images
for idx, imgFile := range imgDirList { for idx, imgFile := range imgDirList {
imgFilename := imgFile.Name() imgFilename := imgFile.Name()
@ -119,6 +240,7 @@ func (o *OCIMultiArch) LoadFromDockerArchives(path string) error {
goos := infos[0] goos := infos[0]
goarch := infos[1] goarch := infos[1]
// Prepare the image conversion
srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker-archive:%s", archivePath)) srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker-archive:%s", archivePath))
if err != nil { if err != nil {
return err return err
@ -131,15 +253,17 @@ func (o *OCIMultiArch) LoadFromDockerArchives(path string) error {
} }
// Convert the docker archive to an oci image
_, err = copy.Image(context.Background(), pctx, dstRef, srcRef, &copy.Options{}) _, err = copy.Image(context.Background(), pctx, dstRef, srcRef, &copy.Options{})
if err != nil { if err != nil {
return err return err
} }
img := OCISystemImage {
path: ociPath, img, err := NewOCISystemImage(ociPath, goos, goarch)
os: goos, if err != nil {
arch: goarch, return err
} }
fmt.Printf("%s -> %s\n", archivePath, img) fmt.Printf("%s -> %s\n", archivePath, img)
o.images = append(o.images, img) o.images = append(o.images, img)
} }
@ -151,54 +275,62 @@ func (o *OCIMultiArch) MergeSystemImages() error {
fmt.Printf("-- merge system images --\n") fmt.Printf("-- merge system images --\n")
multiArchRoot := filepath.Join(o.path, "multi") multiArchRoot := filepath.Join(o.path, "multi")
// Create root // Create the root folder for the OCI multi arch image
err := os.Mkdir(multiArchRoot, 0750) err := os.Mkdir(multiArchRoot, 0750)
if err != nil { if err != nil {
return err return err
} }
// Create blob path // Create the blob folder for the multi arch image
multiBlobPath := filepath.Join(multiArchRoot, 'blobs', 'sha256') multiBlobPath := filepath.Join(multiArchRoot, "blobs", "sha256")
err := os.MkdirAll(multiBlobPath) err = os.MkdirAll(multiBlobPath, 0750)
if err != nil { if err != nil {
return err return err
} }
// Create oci-layout file // Create the oci-layout file
err := NewOCILayout().WriteTo(multiArchRoot) err = NewOCILayout().WriteTo(multiArchRoot)
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("-> oci-layout\n") fmt.Printf("-> oci-layout\n")
// Create index.json // Create the index.json
err, index := NewOCIIndex() idx, err := NewOCIImageIndex(o)
if err != nil {
return err
}
err = idx.WriteTo(multiArchRoot)
if err != nil {
return err
}
o.multi = &idx
fmt.Printf("-> index.json\n") fmt.Printf("-> index.json\n")
// Copy blobs // Copy blobs
for _, img := range o.images { for _, img := range o.images {
blobCounter := 0 blobCounter := 0
blobPath := filepath.Join(img.path, 'blobs', 'sha256') blobPath := filepath.Join(img.path, "blobs", "sha256")
blobList, err := os.ReadDir(blobPath) blobList, err := os.ReadDir(blobPath)
if err != nil { if err != nil {
return err return err
} }
for _, blobFile := range blobList { for _, blobFile := range blobList {
src, err := io.Open(filepath.Join(blobPath, blobFile.Name())) src, err := os.Open(filepath.Join(blobPath, blobFile.Name()))
if err != nil { if err != nil {
return err return err
} }
defer src.Close() defer src.Close()
dst, err := io.Create(filepath.Join(multiBlobPath, blobFile.Name())) dst, err := os.Create(filepath.Join(multiBlobPath, blobFile.Name()))
if err != nil { if err != nil {
return err return err
} }
defer dst.Close() defer dst.Close()
_, err := io.Copy(dst, src) _, err = io.Copy(dst, src)
if err != nil { if err != nil {
return err return err
} }
@ -210,11 +342,144 @@ func (o *OCIMultiArch) MergeSystemImages() error {
return nil return nil
} }
func (o *OCIMultiArch) UploadImage(url string) error { func (o *OCIMultiArch) UploadImageS3(buck *blob.Bucket) error {
fmt.Printf("-- push to the s3 target --\n")
// FS paths
multiArchRoot := filepath.Join(o.path, "multi")
multiBlobPath := filepath.Join(multiArchRoot, "blobs", "sha256")
// Registry URLs
urlPrefix := filepath.Join("v2", o.Name)
manifestPrefix := filepath.Join(urlPrefix, "manifests")
manifestUrl := filepath.Join(manifestPrefix, o.Tag)
blobPrefix := filepath.Join(urlPrefix, "blobs")
// Utils
cutDigestPrefix := len("sha256:")
// Checks
if o.multi == nil {
return errors.New("You try to upload a multiarch image that has not been built yet...")
}
// Upload index
src := filepath.Join(multiArchRoot, "index.json")
up, err := NewUploadFromFS(buck, src)
if err != nil {
return err
}
err = up.ContentType("application/vnd.oci.image.index.v1+json").UploadTo(manifestUrl)
if err != nil {
return err
}
fmt.Printf("[index] index.json -> %s\n", manifestUrl)
// Upload the same index but with its sha256
fd, err := os.Open(src)
if err != nil {
return err
}
defer fd.Close()
h := sha256.New()
if _, err := io.Copy(h, fd); err != nil {
return err
}
digest := fmt.Sprintf("%x", h.Sum(nil))
src = filepath.Join(multiArchRoot, "index.json")
upSha, err := NewUploadFromFS(buck, src)
if err != nil {
return err
}
dst := path.Join(manifestPrefix, "sha256:"+digest)
err = upSha.ContentType("application/vnd.oci.image.index.v1+json").UploadTo(dst)
if err != nil {
return err
}
fmt.Printf("[index] index.json -> %s\n", dst)
// Upload manifest of each system image
for _, m := range o.multi.Manifests {
src := filepath.Join(multiBlobPath, m.Digest[cutDigestPrefix:])
upDigest, err := NewUploadFromFS(buck, src)
if err != nil {
return err
}
dst := path.Join(manifestPrefix, m.Digest)
err = upDigest.ContentType("application/vnd.oci.image.manifest.v1+json").UploadTo(dst)
if err != nil {
return err
}
fmt.Printf("[manifest %s %s] %s -> %s\n", m.Platform.OS, m.Platform.Architecture, src, dst)
}
// Upload blobs from each system image
for _, ref := range o.multi.Manifests {
fullManifest := filepath.Join(multiBlobPath, ref.Digest[cutDigestPrefix:])
m, err := LoadOCIImageManifest(fullManifest)
if err != nil {
return err
}
// Upload config's blob
src := filepath.Join(multiBlobPath, m.Config.Digest[cutDigestPrefix:])
upConf, err := NewUploadFromFS(buck, src)
if err != nil {
return err
}
dst := path.Join(blobPrefix, m.Config.Digest)
err = upConf.UploadTo(dst)
if err != nil {
return err
}
fmt.Printf("[config %s %s] %s -> %s\n", ref.Platform.OS, ref.Platform.Architecture, src, dst)
// Upload layers' blob
counter := 0
for _, lref := range m.Layers {
src := filepath.Join(multiBlobPath, lref.Digest[cutDigestPrefix:])
upLayer, err := NewUploadFromFS(buck, src)
if err != nil {
return err
}
dst := path.Join(blobPrefix, lref.Digest)
err = upLayer.UploadTo(dst)
if err != nil {
return err
}
counter += 1
}
fmt.Printf("[blob %s %s] %d items sent\n", ref.Platform.OS, ref.Platform.Architecture, counter)
}
return nil return nil
} }
type StaticRegistryManager struct {
name string
buck *blob.Bucket
}
func NewStaticRegistryManager(buck *blob.Bucket, name string) *StaticRegistryManager {
return &StaticRegistryManager{
buck: buck,
name: name,
}
}
func (l *StaticRegistryManager) UpdateTagList() error {
fmt.Println("Not yet implemented")
return nil
}
//--- //---
//--- Command logic //--- Command logic
@ -251,9 +516,30 @@ var containerPublishCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
if err = oi.UploadImage(remotePath); err != nil { if strings.HasPrefix(remotePath, "s3:") {
fmt.Println(err) // open bucket
os.Exit(1) bucket, err := blob.OpenBucket(context.Background(), remotePath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer bucket.Close()
// upload image
if err = oi.UploadImageS3(bucket); err != nil {
fmt.Println(err)
os.Exit(1)
}
// update tag
if err = NewStaticRegistryManager(bucket, oi.Name).UpdateTagList(); err != nil {
fmt.Println(err)
os.Exit(1)
}
} else {
fmt.Printf("Protocol not supported for remote path %s. Supported transports are s3:// and docker://\n", remotePath)
os.Exit(1)
} }
fmt.Printf("✅ push succeeded\n") fmt.Printf("✅ push succeeded\n")

View file

@ -210,6 +210,7 @@ func (a *Artifact) Upload(buck *blob.Bucket) error {
type BucketUploader struct { type BucketUploader struct {
bucket *blob.Bucket bucket *blob.Bucket
reader io.ReadCloser reader io.ReadCloser
options *blob.WriterOptions
} }
func NewUploadFromFS(buck *blob.Bucket, path string) (*BucketUploader, error) { func NewUploadFromFS(buck *blob.Bucket, path string) (*BucketUploader, error) {
fd, err := os.Open(path) fd, err := os.Open(path)
@ -230,6 +231,15 @@ func NewUploadFromByte(buck *blob.Bucket, content []byte) *BucketUploader {
} }
} }
func (bu *BucketUploader) ContentType(ct string) *BucketUploader {
if bu.options == nil {
bu.options = &blob.WriterOptions{}
}
bu.options.ContentType = ct
return bu
}
func (bu *BucketUploader) UploadTo(key string) error { func (bu *BucketUploader) UploadTo(key string) error {
// Checks // Checks
if bu.bucket == nil || bu.reader == nil { if bu.bucket == nil || bu.reader == nil {
@ -237,7 +247,7 @@ func (bu *BucketUploader) UploadTo(key string) error {
} }
// Open remote object // Open remote object
w, err := bu.bucket.NewWriter(context.Background(), key, nil) w, err := bu.bucket.NewWriter(context.Background(), key, bu.options)
if err != nil { if err != nil {
return err return err
} }