123 lines
3.9 KiB
Go
123 lines
3.9 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"io"
|
|
"os"
|
|
|
|
reg "reg/internal/reg"
|
|
)
|
|
|
|
// RunPush реализует команду `reg push <local-path> <image>:<tag> [options]`.
|
|
//
|
|
// Опции:
|
|
// -mount-from <src> кросс-репозиторное монтирование blob'ов из <src>
|
|
// (при наличии — сначала пробуем mount, потом загрузку)
|
|
// -chunk-size <bytes> размер чанка для chunked-загрузки (0 = монолитная)
|
|
func RunPush(ctx context.Context, client *reg.RegistryClient, args []string) int {
|
|
if len(args) < 2 {
|
|
reg.PrintError(MsgPushRequiresTwoArgs.String())
|
|
return 1
|
|
}
|
|
localPath := args[0]
|
|
remoteArg := args[1]
|
|
|
|
cmdFlags := flag.NewFlagSet("push", flag.ContinueOnError)
|
|
cmdFlags.SetOutput(io.Discard)
|
|
mountFromFlag := cmdFlags.String("mount-from", "", "source repository for cross-repo blob mount")
|
|
chunkSizeFlag := cmdFlags.Int64("chunk-size", 0, "chunk size in bytes for chunked upload (0 = monolithic)")
|
|
cmdFlags.Usage = func() {}
|
|
if code, ok := parseFlags(cmdFlags, args[2:]); !ok {
|
|
return code
|
|
}
|
|
|
|
if *chunkSizeFlag < 0 {
|
|
reg.PrintError(MsgChunkSizeNonNeg.String())
|
|
return 1
|
|
}
|
|
|
|
remoteRef, err := reg.ParseImageRef(remoteArg)
|
|
if err != nil {
|
|
reg.PrintError(err.Error())
|
|
return 1
|
|
}
|
|
if remoteRef.Tag == "" {
|
|
reg.PrintError(MsgPushTargetNoTag.String())
|
|
return 1
|
|
}
|
|
|
|
layout, cleanup, err := reg.LoadOCILayout(localPath)
|
|
if err != nil {
|
|
reg.PrintError(MsgLoadLayoutError.F(err))
|
|
return 1
|
|
}
|
|
defer cleanup()
|
|
|
|
type blob struct {
|
|
digest string
|
|
size int64
|
|
path string
|
|
}
|
|
blobs := []blob{
|
|
{layout.ConfigDigest, layout.ConfigSize, layout.BlobPath(layout.ConfigDigest)},
|
|
}
|
|
for _, layer := range layout.Layers {
|
|
blobs = append(blobs, blob{layer.Digest, layer.Size, layout.BlobPath(layer.Digest)})
|
|
}
|
|
|
|
for _, b := range blobs {
|
|
// 1. Проверяем, существует ли blob.
|
|
exists, err := client.BlobExists(ctx, remoteRef.Repository, b.digest)
|
|
if err != nil {
|
|
reg.PrintError(MsgBlobCheckError.F(b.digest, err))
|
|
return 1
|
|
}
|
|
if exists {
|
|
continue
|
|
}
|
|
|
|
// 2. Пытаемся смонтировать из другого репозитория (если задан -mount-from).
|
|
if *mountFromFlag != "" {
|
|
mounted, err := client.MountBlob(ctx, remoteRef.Repository, b.digest, *mountFromFlag)
|
|
if err == nil && mounted {
|
|
continue
|
|
}
|
|
// при ошибке или 202 Accepted — обычная загрузка
|
|
}
|
|
|
|
// 3. Загружаем: монолитно или чанками.
|
|
f, err := os.Open(b.path)
|
|
if err != nil {
|
|
reg.PrintError(MsgBlobOpenError.F(b.digest, err))
|
|
return 1
|
|
}
|
|
if *chunkSizeFlag > 0 {
|
|
err = client.UploadBlobChunked(ctx, remoteRef.Repository, b.digest, b.size, f, *chunkSizeFlag)
|
|
} else {
|
|
err = client.UploadBlob(ctx, remoteRef.Repository, b.digest, b.size, f)
|
|
}
|
|
f.Close()
|
|
if err != nil {
|
|
reg.PrintError(MsgBlobUploadError.F(b.digest, err))
|
|
return 1
|
|
}
|
|
}
|
|
|
|
// 4. Загружаем манифест.
|
|
if err := client.PutManifest(ctx, remoteRef.Repository, remoteRef.Tag, layout.ManifestBody, layout.ManifestMediaType); err != nil {
|
|
reg.PrintError(MsgManifestUploadError.F(err))
|
|
return 1
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"repository": remoteRef.Repository,
|
|
"tag": remoteRef.Tag,
|
|
"digest": layout.ManifestDigest,
|
|
"pushed": true,
|
|
}
|
|
reg.PrintJSON(result)
|
|
return 0
|
|
}
|
|
|