albatros/cmd/container.go
2023-04-28 18:20:21 +02:00

282 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, &copy.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)
}