281 lines
6.4 KiB
Go
281 lines
6.4 KiB
Go
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"github.com/containers/image/v5/transports/alltransports"
|
|
"github.com/containers/image/v5/signature"
|
|
"github.com/containers/image/v5/copy"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var pctx *signature.PolicyContext
|
|
const distributionPrefix = "v2"
|
|
|
|
|
|
//---
|
|
//--- Image converter
|
|
type OCIPlatform struct {
|
|
}
|
|
|
|
type OCIImageManifest struct {
|
|
MediaType string `json:"mediaType"`
|
|
Digest string `json:"digest"`
|
|
Size int `json:"size"`
|
|
Platform *OCIPlatform `json:"platform,omitempty"`
|
|
}
|
|
|
|
type OCIImageIndex struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
MediaType string `json:"mediaType"`
|
|
Manifests []OCIImageManifest `json:"manifests"`
|
|
}
|
|
|
|
type OCILayout {
|
|
ImageLayoutVersion string `json:"imageLayoutVersion"`
|
|
}
|
|
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
|
|
}
|
|
|
|
txtPath := filepath.Join(root, "oci-layout")
|
|
err = ioutil.WriteFile(txtPath, txt, 0644)
|
|
return err
|
|
}
|
|
|
|
type OCISystemImage struct {
|
|
path string
|
|
os string
|
|
arch string
|
|
}
|
|
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 {
|
|
name string
|
|
tag string
|
|
path string
|
|
images []OCISystemImage
|
|
}
|
|
|
|
func NewOCIMultiArch(nametag string) (*OCIMultiArch, error) {
|
|
ntspl := strings.Split(nametag, ":")
|
|
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
|
|
}
|
|
|
|
return &OCIMultiArch {
|
|
name: ntspl[0],
|
|
tag: ntspl[1],
|
|
path: tmp,
|
|
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
|
|
}
|
|
|
|
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]
|
|
|
|
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
|
|
}
|
|
|
|
|
|
_, err = copy.Image(context.Background(), pctx, dstRef, srcRef, ©.Options{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
img := OCISystemImage {
|
|
path: ociPath,
|
|
os: goos,
|
|
arch: goarch,
|
|
}
|
|
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")
|
|
|
|
// Create root
|
|
err := os.Mkdir(multiArchRoot, 0750)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create blob path
|
|
multiBlobPath := filepath.Join(multiArchRoot, 'blobs', 'sha256')
|
|
err := os.MkdirAll(multiBlobPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
|
|
// Create oci-layout file
|
|
err := NewOCILayout().WriteTo(multiArchRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("-> oci-layout\n")
|
|
|
|
// Create index.json
|
|
err, index := NewOCIIndex()
|
|
fmt.Printf("-> index.json\n")
|
|
|
|
|
|
// Copy blobs
|
|
for _, img := range o.images {
|
|
blobCounter := 0
|
|
blobPath := filepath.Join(img.path, 'blobs', 'sha256')
|
|
blobList, err := os.ReadDir(blobPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, blobFile := range blobList {
|
|
src, err := io.Open(filepath.Join(blobPath, blobFile.Name()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
dst, err := io.Create(filepath.Join(multiBlobPath, blobFile.Name()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dst.Close()
|
|
|
|
_, err := io.Copy(dst, src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
blobCounter += 1
|
|
|
|
}
|
|
fmt.Printf("%s -> %s (%d items)\n", blobPath, multiBlobPath, blobCounter)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *OCIMultiArch) UploadImage(url string) error {
|
|
return nil
|
|
}
|
|
|
|
|
|
//---
|
|
//--- Command logic
|
|
|
|
var containerCmd = &cobra.Command{
|
|
Use: "container",
|
|
Short: "Manage container images",
|
|
Long: "Publish software on an S3 target following the OCI specification",
|
|
}
|
|
|
|
var containerTag string
|
|
var containerPublishCmd = &cobra.Command{
|
|
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]
|
|
|
|
oi, err := NewOCIMultiArch(containerTag)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
//defer oi.Close()
|
|
|
|
if err = oi.LoadFromDockerArchives(localPath); err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err = oi.MergeSystemImages(); err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if err = oi.UploadImage(remotePath); err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("✅ push succeeded\n")
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
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 {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
containerPublishCmd.Flags().StringVarP(&containerTag, "tag", "t", "", "Tag of the project, eg. albatros:0.9")
|
|
containerPublishCmd.MarkFlagRequired("tag")
|
|
|
|
containerCmd.AddCommand(containerPublishCmd)
|
|
RootCmd.AddCommand(containerCmd)
|
|
}
|