2023-04-28 10:05:22 +00:00
|
|
|
package cmd
|
|
|
|
|
|
|
|
import (
|
2023-04-28 16:20:21 +00:00
|
|
|
"context"
|
2023-05-03 08:52:34 +00:00
|
|
|
"crypto/sha256"
|
2023-05-03 07:52:46 +00:00
|
|
|
"encoding/json"
|
2023-05-03 08:52:34 +00:00
|
|
|
"errors"
|
2023-04-28 10:05:22 +00:00
|
|
|
"fmt"
|
2023-05-03 07:52:46 +00:00
|
|
|
"io"
|
2023-05-03 11:47:53 +00:00
|
|
|
"net/http"
|
2023-04-28 16:20:21 +00:00
|
|
|
"os"
|
2023-05-03 07:52:46 +00:00
|
|
|
"path"
|
2023-04-28 16:20:21 +00:00
|
|
|
"path/filepath"
|
2023-05-03 10:14:06 +00:00
|
|
|
"sort"
|
2023-04-28 16:20:21 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2023-05-03 10:14:06 +00:00
|
|
|
"time"
|
2023-05-03 07:52:46 +00:00
|
|
|
|
2023-05-03 11:47:53 +00:00
|
|
|
"github.com/docker/cli/cli/config"
|
|
|
|
|
|
|
|
"github.com/google/go-containerregistry/pkg/authn"
|
|
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
|
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
|
|
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
"github.com/containers/image/v5/copy"
|
2023-05-03 08:52:34 +00:00
|
|
|
"github.com/containers/image/v5/signature"
|
|
|
|
"github.com/containers/image/v5/transports/alltransports"
|
2023-04-28 10:05:22 +00:00
|
|
|
"github.com/spf13/cobra"
|
2023-05-03 07:52:46 +00:00
|
|
|
"gocloud.dev/blob"
|
|
|
|
_ "gocloud.dev/blob/s3blob"
|
2023-04-28 10:05:22 +00:00
|
|
|
)
|
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
var pctx *signature.PolicyContext
|
2023-04-28 10:05:22 +00:00
|
|
|
|
2023-05-03 08:52:34 +00:00
|
|
|
const distributionPrefix = "v2"
|
2023-04-28 16:20:21 +00:00
|
|
|
|
2023-05-03 11:47:53 +00:00
|
|
|
//---
|
|
|
|
//--- vendored from crane
|
|
|
|
// headerTransport sets headers on outgoing requests.
|
|
|
|
type headerTransport struct {
|
|
|
|
httpHeaders map[string]string
|
|
|
|
inner http.RoundTripper
|
|
|
|
}
|
|
|
|
|
|
|
|
// RoundTrip implements http.RoundTripper.
|
|
|
|
func (ht *headerTransport) RoundTrip(in *http.Request) (*http.Response, error) {
|
|
|
|
for k, v := range ht.httpHeaders {
|
|
|
|
if http.CanonicalHeaderKey(k) == "User-Agent" {
|
|
|
|
// Docker sets this, which is annoying, since we're not docker.
|
|
|
|
// We might want to revisit completely ignoring this.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
in.Header.Set(k, v)
|
|
|
|
}
|
|
|
|
return ht.inner.RoundTrip(in)
|
|
|
|
}
|
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
//---
|
|
|
|
//--- Image converter
|
2023-05-03 07:52:46 +00:00
|
|
|
type OCIImageManifest struct {
|
2023-05-03 08:52:34 +00:00
|
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
|
|
MediaType string `json:"mediaType"`
|
|
|
|
Config OCIRef `json:"config"`
|
|
|
|
Layers []OCIRef `json:layers"`
|
2023-05-03 07:52:46 +00:00
|
|
|
}
|
2023-05-03 08:52:34 +00:00
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
type OCIPlatform struct {
|
2023-05-03 07:52:46 +00:00
|
|
|
Architecture string `json:"architecture"`
|
2023-05-03 08:52:34 +00:00
|
|
|
OS string `json:"os"`
|
2023-04-28 16:20:21 +00:00
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
type OCIRef struct {
|
2023-05-03 08:52:34 +00:00
|
|
|
MediaType string `json:"mediaType"`
|
|
|
|
Digest string `json:"digest"`
|
|
|
|
Size int `json:"size"`
|
|
|
|
Platform *OCIPlatform `json:"platform,omitempty"`
|
2023-04-28 16:20:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type OCIImageIndex struct {
|
2023-05-03 08:52:34 +00:00
|
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
|
|
MediaType string `json:"mediaType"`
|
|
|
|
Manifests []OCIRef `json:"manifests"`
|
2023-05-03 07:52:46 +00:00
|
|
|
}
|
2023-05-03 08:52:34 +00:00
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
func NewOCIImageIndex(o *OCIMultiArch) (OCIImageIndex, error) {
|
2023-05-03 08:52:34 +00:00
|
|
|
idx := OCIImageIndex{
|
2023-05-03 07:52:46 +00:00
|
|
|
SchemaVersion: 2,
|
2023-05-03 08:52:34 +00:00
|
|
|
MediaType: "application/vnd.oci.image.index.v1+json",
|
|
|
|
Manifests: []OCIRef{},
|
2023-05-03 07:52:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2023-04-28 16:20:21 +00:00
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
func LoadOCIImageIndex(path string) (*OCIImageIndex, error) {
|
|
|
|
fd, err := os.Open(filepath.Join(path, "index.json"))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer fd.Close()
|
|
|
|
|
2023-05-03 08:52:34 +00:00
|
|
|
b, err := io.ReadAll(fd)
|
2023-05-03 07:52:46 +00:00
|
|
|
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 {
|
2023-04-28 16:20:21 +00:00
|
|
|
ImageLayoutVersion string `json:"imageLayoutVersion"`
|
|
|
|
}
|
2023-05-03 08:52:34 +00:00
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
func NewOCILayout() OCILayout {
|
|
|
|
return OCILayout{"1.0.0"}
|
|
|
|
}
|
|
|
|
func (o OCILayout) WriteTo(root string) error {
|
|
|
|
txt, err := json.Marshal(o)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-05-03 08:52:34 +00:00
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
txtPath := filepath.Join(root, "oci-layout")
|
2023-05-03 07:52:46 +00:00
|
|
|
err = os.WriteFile(txtPath, txt, 0644)
|
2023-04-28 16:20:21 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
//---- Alba logic
|
2023-04-28 16:20:21 +00:00
|
|
|
type OCISystemImage struct {
|
2023-05-03 08:52:34 +00:00
|
|
|
path string
|
|
|
|
os string
|
|
|
|
arch string
|
2023-05-03 07:52:46 +00:00
|
|
|
index *OCIImageIndex
|
2023-04-28 16:20:21 +00:00
|
|
|
}
|
2023-05-03 08:52:34 +00:00
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
func NewOCISystemImage(path, os, arch string) (OCISystemImage, error) {
|
2023-05-03 08:52:34 +00:00
|
|
|
si := OCISystemImage{
|
|
|
|
path: path,
|
|
|
|
os: os,
|
|
|
|
arch: arch,
|
2023-05-03 07:52:46 +00:00
|
|
|
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
|
2023-05-03 08:52:34 +00:00
|
|
|
si.index.Manifests[0].Platform = &OCIPlatform{
|
2023-05-03 07:52:46 +00:00
|
|
|
Architecture: arch,
|
2023-05-03 08:52:34 +00:00
|
|
|
OS: os,
|
2023-05-03 07:52:46 +00:00
|
|
|
}
|
|
|
|
return si, nil
|
|
|
|
}
|
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
func (o OCISystemImage) String() string {
|
|
|
|
return fmt.Sprintf("[oci system image; os:%s, arch:%s, path:%s]", o.os, o.arch, o.path)
|
|
|
|
}
|
|
|
|
|
|
|
|
type OCIMultiArch struct {
|
2023-05-03 08:52:34 +00:00
|
|
|
Name string
|
|
|
|
Tag string
|
|
|
|
path string
|
|
|
|
multi *OCIImageIndex
|
2023-04-28 16:20:21 +00:00
|
|
|
images []OCISystemImage
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewOCIMultiArch(nametag string) (*OCIMultiArch, error) {
|
2023-05-03 08:52:34 +00:00
|
|
|
ntspl := strings.Split(nametag, ":")
|
2023-04-28 16:20:21 +00:00
|
|
|
if len(ntspl) != 2 {
|
|
|
|
return nil, errors.New("nametag must be of the form 'name:tag'")
|
|
|
|
}
|
|
|
|
|
|
|
|
tmp, err := os.MkdirTemp("", "alba-oci")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-05-03 08:52:34 +00:00
|
|
|
return &OCIMultiArch{
|
|
|
|
Name: ntspl[0],
|
|
|
|
Tag: ntspl[1],
|
|
|
|
path: tmp,
|
2023-04-28 16:20:21 +00:00
|
|
|
images: []OCISystemImage{},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *OCIMultiArch) Close() error {
|
|
|
|
return os.RemoveAll(o.path)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *OCIMultiArch) LoadFromDockerArchives(path string) error {
|
|
|
|
fmt.Printf("-- load docker archives --\n")
|
|
|
|
imgDirList, err := os.ReadDir(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
// Convert all docker archives to oci images
|
2023-04-28 16:20:21 +00:00
|
|
|
for idx, imgFile := range imgDirList {
|
|
|
|
imgFilename := imgFile.Name()
|
|
|
|
|
|
|
|
// Check extension
|
|
|
|
archivePath := filepath.Join(path, imgFilename)
|
|
|
|
if !(strings.HasSuffix(imgFilename, ".tar.gz") || strings.HasSuffix(imgFilename, ".tgz")) {
|
|
|
|
fmt.Printf("skipping %s: not a tar.gz archive\n", archivePath)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check we can extract info from the filename
|
|
|
|
infos := strings.Split(imgFilename, ".")
|
|
|
|
if len(infos) != 3 && len(infos) != 4 {
|
|
|
|
fmt.Printf("skipping %s: format is <goos>.<goarch>.tar.gz\n", archivePath)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
goos := infos[0]
|
|
|
|
goarch := infos[1]
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
// Prepare the image conversion
|
2023-04-28 16:20:21 +00:00
|
|
|
srcRef, err := alltransports.ParseImageName(fmt.Sprintf("docker-archive:%s", archivePath))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
ociPath := filepath.Join(o.path, strconv.FormatInt(int64(idx), 10))
|
|
|
|
dstRef, err := alltransports.ParseImageName(fmt.Sprintf("oci:%s", ociPath))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
// Convert the docker archive to an oci image
|
2023-04-28 16:20:21 +00:00
|
|
|
_, err = copy.Image(context.Background(), pctx, dstRef, srcRef, ©.Options{})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-05-03 07:52:46 +00:00
|
|
|
|
|
|
|
img, err := NewOCISystemImage(ociPath, goos, goarch)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2023-04-28 16:20:21 +00:00
|
|
|
}
|
2023-05-03 07:52:46 +00:00
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
fmt.Printf("%s -> %s\n", archivePath, img)
|
|
|
|
o.images = append(o.images, img)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *OCIMultiArch) MergeSystemImages() error {
|
|
|
|
fmt.Printf("-- merge system images --\n")
|
|
|
|
multiArchRoot := filepath.Join(o.path, "multi")
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
// Create the root folder for the OCI multi arch image
|
2023-04-28 16:20:21 +00:00
|
|
|
err := os.Mkdir(multiArchRoot, 0750)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
// Create the blob folder for the multi arch image
|
|
|
|
multiBlobPath := filepath.Join(multiArchRoot, "blobs", "sha256")
|
|
|
|
err = os.MkdirAll(multiBlobPath, 0750)
|
2023-04-28 16:20:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
// Create the oci-layout file
|
|
|
|
err = NewOCILayout().WriteTo(multiArchRoot)
|
2023-04-28 16:20:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Printf("-> oci-layout\n")
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
// Create the index.json
|
|
|
|
idx, err := NewOCIImageIndex(o)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = idx.WriteTo(multiArchRoot)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
o.multi = &idx
|
2023-04-28 16:20:21 +00:00
|
|
|
fmt.Printf("-> index.json\n")
|
|
|
|
|
|
|
|
// Copy blobs
|
|
|
|
for _, img := range o.images {
|
|
|
|
blobCounter := 0
|
2023-05-03 07:52:46 +00:00
|
|
|
blobPath := filepath.Join(img.path, "blobs", "sha256")
|
2023-04-28 16:20:21 +00:00
|
|
|
blobList, err := os.ReadDir(blobPath)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
for _, blobFile := range blobList {
|
2023-05-03 07:52:46 +00:00
|
|
|
src, err := os.Open(filepath.Join(blobPath, blobFile.Name()))
|
2023-04-28 16:20:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer src.Close()
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
dst, err := os.Create(filepath.Join(multiBlobPath, blobFile.Name()))
|
2023-04-28 16:20:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer dst.Close()
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
_, err = io.Copy(dst, src)
|
2023-04-28 16:20:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
blobCounter += 1
|
|
|
|
|
|
|
|
}
|
|
|
|
fmt.Printf("%s -> %s (%d items)\n", blobPath, multiBlobPath, blobCounter)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-05-03 11:47:53 +00:00
|
|
|
func (o *OCIMultiArch) UploadImageRegistry(tag string) error {
|
|
|
|
fmt.Println("--- push to registry ---")
|
|
|
|
multiArchRoot := filepath.Join(o.path, "multi")
|
|
|
|
|
|
|
|
img, err := layout.ImageIndexFromPath(multiArchRoot)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("loading %s as OCI layout: %w", multiArchRoot, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ref, err := name.ParseReference(tag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build transport
|
|
|
|
transport := remote.DefaultTransport.(*http.Transport).Clone()
|
|
|
|
var rt http.RoundTripper = transport
|
|
|
|
cf, err := config.Load(os.Getenv("DOCKER_CONFIG"))
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("failed to read config file: %v", err)
|
|
|
|
} else if len(cf.HTTPHeaders) != 0 {
|
|
|
|
rt = &headerTransport{
|
|
|
|
inner: rt,
|
|
|
|
httpHeaders: cf.HTTPHeaders,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := remote.WriteIndex(ref, img, remote.WithTransport(rt), remote.WithAuthFromKeychain(authn.DefaultKeychain)); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
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
|
2023-05-03 08:52:34 +00:00
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
}
|
|
|
|
fmt.Printf("[blob %s %s] %d items sent\n", ref.Platform.OS, ref.Platform.Architecture, counter)
|
|
|
|
}
|
2023-05-03 08:52:34 +00:00
|
|
|
|
2023-04-28 16:20:21 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
type StaticRegistryManager struct {
|
2023-05-03 11:47:53 +00:00
|
|
|
name string
|
|
|
|
buck *blob.Bucket
|
2023-05-03 10:14:06 +00:00
|
|
|
images []ImageDescriptor
|
|
|
|
}
|
|
|
|
|
|
|
|
type ImageDescriptor struct {
|
2023-05-03 11:47:53 +00:00
|
|
|
Tag string
|
2023-05-03 10:14:06 +00:00
|
|
|
Date time.Time
|
2023-05-03 07:52:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewStaticRegistryManager(buck *blob.Bucket, name string) *StaticRegistryManager {
|
|
|
|
return &StaticRegistryManager{
|
|
|
|
buck: buck,
|
|
|
|
name: name,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-03 08:51:36 +00:00
|
|
|
type TagList struct {
|
2023-05-03 08:52:34 +00:00
|
|
|
Name string `json:"name"`
|
2023-05-03 08:51:36 +00:00
|
|
|
Tags []string `json:"tags"`
|
|
|
|
}
|
|
|
|
|
2023-05-03 10:14:06 +00:00
|
|
|
func (l *StaticRegistryManager) Scan() error {
|
2023-05-03 08:51:36 +00:00
|
|
|
digestPrefix := "sha256:"
|
|
|
|
cutDigestPrefix := len(digestPrefix)
|
|
|
|
|
|
|
|
iter := l.buck.List(&blob.ListOptions{
|
2023-05-03 08:52:34 +00:00
|
|
|
Prefix: fmt.Sprintf("v2/%s/manifests/", l.name),
|
2023-05-03 08:51:36 +00:00
|
|
|
Delimiter: "/",
|
|
|
|
})
|
|
|
|
for {
|
|
|
|
obj, err := iter.Next(context.Background())
|
|
|
|
if err == io.EOF {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if err != nil {
|
2023-05-03 10:14:06 +00:00
|
|
|
return err
|
2023-05-03 08:51:36 +00:00
|
|
|
}
|
2023-05-03 08:52:34 +00:00
|
|
|
|
2023-05-03 08:51:36 +00:00
|
|
|
ksplit := strings.Split(obj.Key, "/")
|
|
|
|
if len(ksplit) < 1 {
|
2023-05-03 10:14:06 +00:00
|
|
|
return errors.New(fmt.Sprintf("Invalid key name %s", obj.Key))
|
2023-05-03 08:51:36 +00:00
|
|
|
}
|
|
|
|
fname := ksplit[len(ksplit)-1]
|
|
|
|
|
|
|
|
if len(fname) >= cutDigestPrefix && fname[:cutDigestPrefix] == digestPrefix {
|
|
|
|
// we ignore sha256 addressed manifests
|
2023-05-03 08:52:34 +00:00
|
|
|
continue
|
2023-05-03 08:51:36 +00:00
|
|
|
}
|
2023-05-03 11:47:53 +00:00
|
|
|
id := ImageDescriptor{
|
|
|
|
Tag: fname,
|
2023-05-03 10:14:06 +00:00
|
|
|
Date: obj.ModTime,
|
|
|
|
}
|
|
|
|
l.images = append(l.images, id)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *StaticRegistryManager) TagList() TagList {
|
|
|
|
// Sort by date desc
|
|
|
|
sort.Slice(l.images, func(i, j int) bool {
|
|
|
|
return l.images[i].Date.After(l.images[j].Date)
|
|
|
|
})
|
|
|
|
|
|
|
|
// Build tagList
|
2023-05-03 11:47:53 +00:00
|
|
|
tagList := TagList{
|
2023-05-03 10:14:06 +00:00
|
|
|
Name: l.name,
|
2023-05-03 08:51:36 +00:00
|
|
|
}
|
2023-05-03 10:14:06 +00:00
|
|
|
for _, img := range l.images {
|
|
|
|
tagList.Tags = append(tagList.Tags, img.Tag)
|
|
|
|
}
|
|
|
|
return tagList
|
2023-05-03 08:51:36 +00:00
|
|
|
}
|
|
|
|
|
2023-05-03 07:52:46 +00:00
|
|
|
func (l *StaticRegistryManager) UpdateTagList() error {
|
2023-05-03 08:51:36 +00:00
|
|
|
fmt.Printf("--- update taglist ---\n")
|
|
|
|
|
2023-05-03 10:14:06 +00:00
|
|
|
err := l.Scan()
|
2023-05-03 08:51:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-05-03 10:14:06 +00:00
|
|
|
|
|
|
|
tagList := l.TagList()
|
2023-05-03 08:51:36 +00:00
|
|
|
fmt.Printf("computed tag list: %v\n", tagList)
|
|
|
|
|
|
|
|
txt, err := json.Marshal(tagList)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
dst := path.Join("v2", l.name, "tags", "list")
|
|
|
|
err = NewUploadFromByte(l.buck, txt).ContentType("application/json").UploadTo(dst)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
fmt.Printf("taglist -> %s\n", dst)
|
2023-05-03 07:52:46 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-28 10:05:22 +00:00
|
|
|
//---
|
|
|
|
//--- Command logic
|
|
|
|
|
|
|
|
var containerCmd = &cobra.Command{
|
2023-05-03 08:52:34 +00:00
|
|
|
Use: "container",
|
|
|
|
Short: "Manage container images",
|
|
|
|
Long: "Publish software on an S3 target following the OCI specification",
|
2023-04-28 10:05:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var containerTag string
|
|
|
|
var containerPublishCmd = &cobra.Command{
|
2023-05-03 08:52:34 +00:00
|
|
|
Use: "push [folder] [remote]", // https://gocloud.dev/howto/blob/#s3-compatible
|
|
|
|
Short: "Publish a container image",
|
|
|
|
Long: "Copy .tar.gz files in the specified folder on the S3 target so that they match the OCI distribution specification",
|
|
|
|
Args: cobra.ExactArgs(2),
|
|
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
localPath := args[0]
|
|
|
|
remotePath := args[1]
|
|
|
|
|
2023-05-03 11:47:53 +00:00
|
|
|
dockerProto := "docker://"
|
|
|
|
dockerProtoCut := len(dockerProto)
|
|
|
|
|
2023-05-03 08:52:34 +00:00
|
|
|
oi, err := NewOCIMultiArch(containerTag)
|
2023-05-03 07:52:46 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2023-05-03 08:52:34 +00:00
|
|
|
defer oi.Close()
|
2023-05-03 07:52:46 +00:00
|
|
|
|
2023-05-03 08:52:34 +00:00
|
|
|
if err = oi.LoadFromDockerArchives(localPath); err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
os.Exit(1)
|
2023-05-03 07:52:46 +00:00
|
|
|
}
|
|
|
|
|
2023-05-03 08:52:34 +00:00
|
|
|
if err = oi.MergeSystemImages(); err != nil {
|
2023-05-03 07:52:46 +00:00
|
|
|
fmt.Println(err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
2023-05-03 11:47:53 +00:00
|
|
|
if strings.HasPrefix(remotePath, "s3://") {
|
2023-05-03 08:52:34 +00:00
|
|
|
// open bucket
|
|
|
|
bucket, err := blob.OpenBucket(context.Background(), remotePath)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
defer bucket.Close()
|
2023-04-28 10:05:22 +00:00
|
|
|
|
2023-05-03 08:52:34 +00:00
|
|
|
// 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)
|
2023-05-03 11:47:53 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
} else if strings.HasPrefix(remotePath, dockerProto) {
|
|
|
|
if err = oi.UploadImageRegistry(remotePath[dockerProtoCut:]); err != nil {
|
|
|
|
fmt.Println(err)
|
2023-05-03 08:52:34 +00:00
|
|
|
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")
|
|
|
|
},
|
2023-04-28 10:05:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
2023-04-28 16:20:21 +00:00
|
|
|
var err error
|
|
|
|
|
|
|
|
// @FIXME: this policy feature is probably here for something, so we should not bypass it
|
|
|
|
// but as there is no documentation around the error on how to easily and quickly doing it the right way,
|
|
|
|
// I am just disabling it. Thanks again security people for yet another convoluted incomprehensible undocumented stuff.
|
|
|
|
policy := &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}}
|
|
|
|
pctx, err = signature.NewPolicyContext(policy)
|
|
|
|
if err != nil {
|
2023-05-03 08:52:34 +00:00
|
|
|
fmt.Println(err)
|
|
|
|
os.Exit(1)
|
2023-04-28 16:20:21 +00:00
|
|
|
}
|
|
|
|
|
2023-04-28 10:05:22 +00:00
|
|
|
containerPublishCmd.Flags().StringVarP(&containerTag, "tag", "t", "", "Tag of the project, eg. albatros:0.9")
|
|
|
|
containerPublishCmd.MarkFlagRequired("tag")
|
|
|
|
|
|
|
|
containerCmd.AddCommand(containerPublishCmd)
|
|
|
|
RootCmd.AddCommand(containerCmd)
|
|
|
|
}
|