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 ..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) }