From 068d4ad53fde9a872563bec8019628e939f34419 Mon Sep 17 00:00:00 2001 From: aajkl Date: Wed, 3 Jul 2024 19:59:05 +0800 Subject: [PATCH] merge vmimage pkg (#24) * merge vmimage pkg * replace string literal with constant var * fix review issues --- .gitignore | 1 + Makefile | 1 + cmd/image/image.go | 4 +- configs/config.go | 2 +- go.mod | 5 +- go.sum | 2 - internal/models/guest.go | 4 +- internal/rpc/grpc_app.go | 2 +- internal/service/boar/boar.go | 4 +- internal/service/boar/image.go | 4 +- internal/service/boar/raw_engine.go | 2 +- internal/service/mocks/Service.go | 2 +- internal/service/service.go | 2 +- internal/virt/guest/bot.go | 4 +- internal/virt/guest/guest.go | 4 +- internal/virt/guest/guest_test.go | 4 +- internal/virt/guest/mocks/Bot.go | 2 +- internal/volume/local/volume.go | 6 +- internal/volume/mocks/Volume.go | 2 +- internal/volume/rbd/volume.go | 4 +- internal/volume/volume.go | 2 +- pkg/vmimage/docker/docker.go | 233 ++++++++++++++++++++++++++++ pkg/vmimage/factory/factory.go | 152 ++++++++++++++++++ pkg/vmimage/image.go | 19 +++ pkg/vmimage/mocks/Manager.go | 217 ++++++++++++++++++++++++++ pkg/vmimage/types/config.go | 64 ++++++++ pkg/vmimage/types/const.go | 7 + pkg/vmimage/types/image.go | 74 +++++++++ pkg/vmimage/utils/image.go | 83 ++++++++++ pkg/vmimage/utils/image_test.go | 39 +++++ pkg/vmimage/utils/utils.go | 26 ++++ pkg/vmimage/vmihub/vmihub.go | 163 +++++++++++++++++++ 32 files changed, 1108 insertions(+), 32 deletions(-) create mode 100644 pkg/vmimage/docker/docker.go create mode 100644 pkg/vmimage/factory/factory.go create mode 100644 pkg/vmimage/image.go create mode 100644 pkg/vmimage/mocks/Manager.go create mode 100644 pkg/vmimage/types/config.go create mode 100644 pkg/vmimage/types/const.go create mode 100644 pkg/vmimage/types/image.go create mode 100644 pkg/vmimage/utils/image.go create mode 100644 pkg/vmimage/utils/image_test.go create mode 100644 pkg/vmimage/utils/utils.go create mode 100644 pkg/vmimage/vmihub/vmihub.go diff --git a/.gitignore b/.gitignore index 44d7609..00c034a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ yavirt .idea/ .vscode /tmp +.secrets \ No newline at end of file diff --git a/Makefile b/Makefile index b47891c..b24a6d0 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,7 @@ mock: deps mockery --dir pkg/sh --output pkg/sh/mocks --name Shell mockery --dir pkg/store --output pkg/store/mocks --name Store mockery --dir pkg/utils --output pkg/utils/mocks --name Locker + mockery --dir pkg/vmimage --output mocks --name Manager mockery --dir internal/virt/agent --output internal/virt/agent/mocks --all mockery --dir internal/virt/domain --output internal/virt/domain/mocks --name Domain mockery --dir internal/virt/guest --output internal/virt/guest/mocks --name Bot diff --git a/cmd/image/image.go b/cmd/image/image.go index cd211a7..334421e 100644 --- a/cmd/image/image.go +++ b/cmd/image/image.go @@ -15,7 +15,7 @@ import ( "github.com/projecteru2/yavirt/cmd/run" "github.com/projecteru2/yavirt/configs" "github.com/projecteru2/yavirt/internal/utils" - vmiFact "github.com/yuyang0/vmimage/factory" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" ) // Command . @@ -144,7 +144,7 @@ func add(c *cli.Context, _ run.Runtime) error { return err } fmt.Printf("*** Prepare image\n") - if rc, err := vmiFact.Prepare(filePath, img); err != nil { + if rc, err := vmiFact.Prepare(c.Context, filePath, img); err != nil { return errors.Wrap(err, "") } else { //nolint defer rc.Close() diff --git a/configs/config.go b/configs/config.go index 445c282..d9a6366 100644 --- a/configs/config.go +++ b/configs/config.go @@ -15,7 +15,7 @@ import ( "github.com/urfave/cli/v2" coretypes "github.com/projecteru2/core/types" - vmitypes "github.com/yuyang0/vmimage/types" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" ) var ( diff --git a/go.mod b/go.mod index ba2ef85..d11963c 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/containernetworking/cni v1.1.2 github.com/deckarep/golang-set/v2 v2.3.1 github.com/digitalocean/go-libvirt v0.0.0-20221205150000-2939327a8519 + github.com/docker/docker v24.0.9+incompatible github.com/dustin/go-humanize v1.0.1 github.com/emirpasic/gods v1.18.1 github.com/florianl/go-tc v0.4.2 @@ -38,6 +39,7 @@ require ( github.com/projecteru2/core v0.0.0-20240614132727-08e4fbc219d1 github.com/projecteru2/libyavirt v0.0.0-20231128023216-96fef06a6ca4 github.com/projecteru2/resource-storage v0.0.0-20230206062354-d828802f6b96 + github.com/projecteru2/vmihub v0.0.0-20240702045253-4fa15dba054f github.com/prometheus-community/pro-bing v0.4.0 github.com/prometheus/client_golang v1.16.0 github.com/robfig/cron/v3 v3.0.1 @@ -52,7 +54,6 @@ require ( github.com/yuyang0/resource-bandwidth v0.0.0-20231102113253-8e47795c92e5 github.com/yuyang0/resource-gpu v0.0.0-20231026065700-1577d804efa8 github.com/yuyang0/resource-rbd v0.0.2-0.20230701090628-cb86da0f60b9 - github.com/yuyang0/vmimage v0.0.0-20240628091041-9f45a357a3ae go.etcd.io/etcd v3.3.27+incompatible go.etcd.io/etcd/client/v3 v3.5.12 go.etcd.io/etcd/tests/v3 v3.5.12 @@ -90,7 +91,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v24.0.9+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.10.2 // indirect @@ -150,7 +150,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/projectcalico/go-json v0.0.0-20161128004156-6219dc7339ba // indirect github.com/projectcalico/go-yaml-wrapper v0.0.0-20191112210931-090425220c54 // indirect - github.com/projecteru2/vmihub v0.0.0-20240702045253-4fa15dba054f // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect diff --git a/go.sum b/go.sum index d1020d2..44ee2cb 100644 --- a/go.sum +++ b/go.sum @@ -517,8 +517,6 @@ github.com/yuyang0/resource-gpu v0.0.0-20231026065700-1577d804efa8 h1:U1GBBWRCG0 github.com/yuyang0/resource-gpu v0.0.0-20231026065700-1577d804efa8/go.mod h1:oggnae33QHkm9k2Xd0J4BFjdIV1VhPdpm4VUujYUvo0= github.com/yuyang0/resource-rbd v0.0.2-0.20230701090628-cb86da0f60b9 h1:2La8T7mqVy98jyAkwxIN9gB+Akx3qbLGmVEtleaxND4= github.com/yuyang0/resource-rbd v0.0.2-0.20230701090628-cb86da0f60b9/go.mod h1:ANjyr7r+YfKtpWiIsZPzF7+krI55Uf84R9AvbNr5WAg= -github.com/yuyang0/vmimage v0.0.0-20240628091041-9f45a357a3ae h1:qsuhmk0vb2uNRdWsI+23DaOODto0/fG8tmEnqwHmjCA= -github.com/yuyang0/vmimage v0.0.0-20240628091041-9f45a357a3ae/go.mod h1:sx0f5ijzfuwsxQnDlU8CpRbEzAoQu6TxpEKN6gozBAw= go.etcd.io/etcd v3.3.27+incompatible h1:5hMrpf6REqTHV2LW2OclNpRtxI0k9ZplMemJsMSWju0= go.etcd.io/etcd v3.3.27+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c= diff --git a/internal/models/guest.go b/internal/models/guest.go index 8bd4428..0fdd061 100644 --- a/internal/models/guest.go +++ b/internal/models/guest.go @@ -22,10 +22,10 @@ import ( "github.com/projecteru2/yavirt/pkg/store" "github.com/projecteru2/yavirt/pkg/terrors" "github.com/projecteru2/yavirt/pkg/utils" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" bdtypes "github.com/yuyang0/resource-bandwidth/bandwidth/types" gputypes "github.com/yuyang0/resource-gpu/gpu/types" - vmiFact "github.com/yuyang0/vmimage/factory" - vmitypes "github.com/yuyang0/vmimage/types" ) // Guest indicates a virtual machine. diff --git a/internal/rpc/grpc_app.go b/internal/rpc/grpc_app.go index 739e8c0..0a1a394 100644 --- a/internal/rpc/grpc_app.go +++ b/internal/rpc/grpc_app.go @@ -17,7 +17,7 @@ import ( "github.com/projecteru2/yavirt/internal/service" intertypes "github.com/projecteru2/yavirt/internal/types" "github.com/projecteru2/yavirt/internal/utils" - vmiFact "github.com/yuyang0/vmimage/factory" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" ) // GRPCYavirtd . diff --git a/internal/service/boar/boar.go b/internal/service/boar/boar.go index fda3212..d9eab02 100644 --- a/internal/service/boar/boar.go +++ b/internal/service/boar/boar.go @@ -31,8 +31,8 @@ import ( "github.com/projecteru2/yavirt/pkg/notify/bison" "github.com/projecteru2/yavirt/pkg/store" "github.com/projecteru2/yavirt/pkg/utils" - vmiFact "github.com/yuyang0/vmimage/factory" - vmitypes "github.com/yuyang0/vmimage/types" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" ) // Boar . diff --git a/internal/service/boar/image.go b/internal/service/boar/image.go index 1e9f2f9..6fd93f8 100644 --- a/internal/service/boar/image.go +++ b/internal/service/boar/image.go @@ -8,8 +8,8 @@ import ( "strings" "github.com/cockroachdb/errors" - vmiFact "github.com/yuyang0/vmimage/factory" - vmitypes "github.com/yuyang0/vmimage/types" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" ) func (svc *Boar) PushImage(ctx context.Context, imgName string, force bool) (rc io.ReadCloser, err error) { diff --git a/internal/service/boar/raw_engine.go b/internal/service/boar/raw_engine.go index e170131..38c5a0e 100644 --- a/internal/service/boar/raw_engine.go +++ b/internal/service/boar/raw_engine.go @@ -12,7 +12,7 @@ import ( intertypes "github.com/projecteru2/yavirt/internal/types" "github.com/projecteru2/yavirt/internal/vmcache" volFact "github.com/projecteru2/yavirt/internal/volume/factory" - vmiFact "github.com/yuyang0/vmimage/factory" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" ) type VMParams struct { diff --git a/internal/service/mocks/Service.go b/internal/service/mocks/Service.go index fc55029..8b36472 100644 --- a/internal/service/mocks/Service.go +++ b/internal/service/mocks/Service.go @@ -13,7 +13,7 @@ import ( utils "github.com/projecteru2/yavirt/internal/utils" - vmimagetypes "github.com/yuyang0/vmimage/types" + vmimagetypes "github.com/projecteru2/yavirt/pkg/vmimage/types" ) // Service is an autogenerated mock type for the Service type diff --git a/internal/service/service.go b/internal/service/service.go index 83f85f1..f9a5417 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -7,7 +7,7 @@ import ( "github.com/projecteru2/libyavirt/types" intertypes "github.com/projecteru2/yavirt/internal/types" "github.com/projecteru2/yavirt/internal/utils" - vmitypes "github.com/yuyang0/vmimage/types" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" ) // Service interface diff --git a/internal/virt/guest/bot.go b/internal/virt/guest/bot.go index 31ba3a1..289fbfe 100644 --- a/internal/virt/guest/bot.go +++ b/internal/virt/guest/bot.go @@ -20,8 +20,8 @@ import ( "github.com/projecteru2/yavirt/pkg/libvirt" "github.com/projecteru2/yavirt/pkg/terrors" "github.com/projecteru2/yavirt/pkg/utils" - vmiFact "github.com/yuyang0/vmimage/factory" - vmitypes "github.com/yuyang0/vmimage/types" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" ) // Bot . diff --git a/internal/virt/guest/guest.go b/internal/virt/guest/guest.go index a766b4e..27a520a 100644 --- a/internal/virt/guest/guest.go +++ b/internal/virt/guest/guest.go @@ -27,9 +27,9 @@ import ( "github.com/projecteru2/yavirt/pkg/libvirt" "github.com/projecteru2/yavirt/pkg/terrors" "github.com/projecteru2/yavirt/pkg/utils" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" gputypes "github.com/yuyang0/resource-gpu/gpu/types" - vmiFact "github.com/yuyang0/vmimage/factory" - vmitypes "github.com/yuyang0/vmimage/types" ) // Guest . diff --git a/internal/virt/guest/guest_test.go b/internal/virt/guest/guest_test.go index 902b3e7..07222a9 100644 --- a/internal/virt/guest/guest_test.go +++ b/internal/virt/guest/guest_test.go @@ -26,9 +26,9 @@ import ( "github.com/projecteru2/yavirt/pkg/test/mock" "github.com/projecteru2/yavirt/pkg/utils" utilmocks "github.com/projecteru2/yavirt/pkg/utils/mocks" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" gputypes "github.com/yuyang0/resource-gpu/gpu/types" - vmiFact "github.com/yuyang0/vmimage/factory" - vmitypes "github.com/yuyang0/vmimage/types" ) const ( diff --git a/internal/virt/guest/mocks/Bot.go b/internal/virt/guest/mocks/Bot.go index 374e3b1..ebf5fcb 100644 --- a/internal/virt/guest/mocks/Bot.go +++ b/internal/virt/guest/mocks/Bot.go @@ -15,7 +15,7 @@ import ( pkglibvirt "github.com/projecteru2/yavirt/pkg/libvirt" - types "github.com/yuyang0/vmimage/types" + types "github.com/projecteru2/yavirt/pkg/vmimage/types" volume "github.com/projecteru2/yavirt/internal/volume" ) diff --git a/internal/volume/local/volume.go b/internal/volume/local/volume.go index c2d33cc..9de8d8c 100644 --- a/internal/volume/local/volume.go +++ b/internal/volume/local/volume.go @@ -22,8 +22,8 @@ import ( "github.com/projecteru2/yavirt/pkg/sh" "github.com/projecteru2/yavirt/pkg/terrors" "github.com/projecteru2/yavirt/pkg/utils" - vmiFact "github.com/yuyang0/vmimage/factory" - vmitypes "github.com/yuyang0/vmimage/types" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" ) var ( @@ -301,7 +301,7 @@ func (v *Volume) CaptureImage(imgName string) (uimg *vmitypes.Image, err error) if err != nil { return nil, err } - rc, err := vmiFact.Prepare(orig, uimg) + rc, err := vmiFact.Prepare(context.TODO(), orig, uimg) if err != nil { return nil, errors.Wrap(err, "") } diff --git a/internal/volume/mocks/Volume.go b/internal/volume/mocks/Volume.go index 1f81265..b7fb720 100644 --- a/internal/volume/mocks/Volume.go +++ b/internal/volume/mocks/Volume.go @@ -11,7 +11,7 @@ import ( mock "github.com/stretchr/testify/mock" - types "github.com/yuyang0/vmimage/types" + types "github.com/projecteru2/yavirt/pkg/vmimage/types" ) // Volume is an autogenerated mock type for the Volume type diff --git a/internal/volume/rbd/volume.go b/internal/volume/rbd/volume.go index ca1d732..7415086 100644 --- a/internal/volume/rbd/volume.go +++ b/internal/volume/rbd/volume.go @@ -20,10 +20,10 @@ import ( "github.com/projecteru2/yavirt/internal/virt/guestfs" "github.com/projecteru2/yavirt/internal/virt/guestfs/gfsx" "github.com/projecteru2/yavirt/internal/volume/base" + vmiFact "github.com/projecteru2/yavirt/pkg/vmimage/factory" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" libguestfs "github.com/projecteru2/yavirt/third_party/guestfs" rbdtypes "github.com/yuyang0/resource-rbd/rbd/types" - vmiFact "github.com/yuyang0/vmimage/factory" - vmitypes "github.com/yuyang0/vmimage/types" ) var ( diff --git a/internal/volume/volume.go b/internal/volume/volume.go index 2f18f01..07aba66 100644 --- a/internal/volume/volume.go +++ b/internal/volume/volume.go @@ -7,7 +7,7 @@ import ( "github.com/projecteru2/yavirt/internal/meta" "github.com/projecteru2/yavirt/internal/virt/guestfs" "github.com/projecteru2/yavirt/internal/volume/base" - vmitypes "github.com/yuyang0/vmimage/types" + vmitypes "github.com/projecteru2/yavirt/pkg/vmimage/types" ) type Volume interface { //nolint:interfacebloat diff --git a/pkg/vmimage/docker/docker.go b/pkg/vmimage/docker/docker.go new file mode 100644 index 0000000..31a4187 --- /dev/null +++ b/pkg/vmimage/docker/docker.go @@ -0,0 +1,233 @@ +package docker + +import ( + "context" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/docker/docker/api/types" + engineapi "github.com/docker/docker/client" + "github.com/docker/docker/pkg/archive" + "github.com/pkg/errors" + pkgtypes "github.com/projecteru2/yavirt/pkg/vmimage/types" + "github.com/projecteru2/yavirt/pkg/vmimage/utils" +) + +const ( + destImgName = "vm.img" + dockerCliVersion = "1.35" +) + +type Manager struct { + cfg *pkgtypes.Config + cli *engineapi.Client +} + +func NewManager(config *pkgtypes.Config) (m *Manager, err error) { + cli, err := makeDockerClient(config.Docker.Endpoint) + if err != nil { + return nil, err + } + m = &Manager{ + cfg: config, + cli: cli, + } + return m, nil +} + +func (mgr *Manager) ListLocalImages(ctx context.Context, user string) ([]*pkgtypes.Image, error) { + images, err := mgr.cli.ImageList(ctx, types.ImageListOptions{}) + if err != nil { + return nil, err + } + var ans []*pkgtypes.Image + prefix := path.Join(mgr.cfg.Docker.Prefix, user) + for _, dockerImg := range images { + for _, repoTag := range dockerImg.RepoTags { + if strings.HasPrefix(repoTag, prefix) { + fullname := strings.TrimPrefix(repoTag, prefix) + fullname = strings.TrimPrefix(fullname, "/") + fullname = strings.TrimPrefix(fullname, "library/") + img, _ := pkgtypes.NewImage(fullname) + ans = append(ans, img) + } + } + } + return ans, nil +} + +func (mgr *Manager) LoadImage(ctx context.Context, imgName string) (img *pkgtypes.Image, err error) { + if img, err = pkgtypes.NewImage(imgName); err != nil { + return nil, err + } + rc, err := mgr.Pull(ctx, img, pkgtypes.PullPolicyAlways) + if err != nil { + return nil, err + } + if err := utils.EnsureReaderClosed(rc); err != nil { + return nil, err + } + if err := mgr.loadMetadata(ctx, img); err != nil { + return nil, err + } + return img, nil +} + +// Prepare prepares the image for use by creating a Dockerfile and building a Docker image. +// +// Parameters: +// - fname: a local filename or an url +// +// Returns: +// - io.ReadCloser: a ReadCloser to read the prepared image. +// - error: an error if any occurred during the preparation process. +func (mgr *Manager) Prepare(ctx context.Context, fname string, img *pkgtypes.Image) (io.ReadCloser, error) { + cli := mgr.cli + baseDir := filepath.Dir(fname) + baseName := filepath.Base(fname) + digest := "" + tarOpts := &archive.TarOptions{ + IncludeFiles: []string{baseName, "Dockerfile.yavirt"}, + Compression: archive.Uncompressed, + NoLchown: true, + } + if u, err := url.Parse(fname); err == nil && u.Scheme != "" && u.Host != "" { + tmpDir, err := os.MkdirTemp(os.TempDir(), "image-prepare-") + if err != nil { + return nil, err + } + defer os.RemoveAll(tmpDir) + baseDir = tmpDir + baseName = fname + tarOpts.IncludeFiles = []string{"Dockerfile.yavirt"} + if digest, err = httpGetSHA256(ctx, fname); err != nil { + return nil, err + } + } else { + if digest, err = utils.CalcDigestOfFile(fname); err != nil { + return nil, err + } + } + dockerfile := fmt.Sprintf("FROM scratch\nLABEL SHA256=%s\nADD %s /%s", digest, baseName, destImgName) + if err := os.WriteFile(filepath.Join(baseDir, "Dockerfile.yavirt"), []byte(dockerfile), 0600); err != nil { + return nil, err + } + defer os.Remove(filepath.Join(baseDir, "Dockerfile.yavirt")) + + // Create a build context from the specified directory + buildContext, err := archive.TarWithOptions(baseDir, tarOpts) + if err != nil { + return nil, err + } + + // Build the Docker image using the build context + buildOptions := types.ImageBuildOptions{ + Context: buildContext, + Dockerfile: "Dockerfile.yavirt", // Use the default Dockerfile name + Tags: []string{mgr.dockerImageName(img)}, + } + + resp, err := cli.ImageBuild(ctx, buildContext, buildOptions) + if err != nil { + return nil, err + } + return resp.Body, nil +} + +func (mgr *Manager) Pull(ctx context.Context, img *pkgtypes.Image, _ pkgtypes.PullPolicy) (io.ReadCloser, error) { + cli, cfg := mgr.cli, mgr.cfg + return cli.ImagePull(ctx, mgr.dockerImageName(img), types.ImagePullOptions{ + RegistryAuth: cfg.Docker.Auth, + }) +} + +func (mgr *Manager) Push(ctx context.Context, img *pkgtypes.Image, force bool) (io.ReadCloser, error) { + cli, cfg := mgr.cli, mgr.cfg + return cli.ImagePush(ctx, mgr.dockerImageName(img), types.ImagePushOptions{ + RegistryAuth: cfg.Docker.Auth, + All: force, + }) +} + +func (mgr *Manager) RemoveLocal(ctx context.Context, img *pkgtypes.Image) error { + cli := mgr.cli + _, err := cli.ImageRemove(ctx, mgr.dockerImageName(img), types.ImageRemoveOptions{ + Force: true, // Remove even if the image is in use + PruneChildren: true, // Prune dependent child images + }) + return err +} + +func (mgr *Manager) CheckHealth(_ context.Context) error { + parts := strings.SplitN(mgr.cfg.Docker.Prefix, "/", 2) + if len(parts) >= 2 && strings.Contains(parts[0], ".") { + if err := utils.IPReachable(parts[0], time.Second); err != nil { + return errors.Wrapf(err, "failed to ping image hub %s", parts[0]) + } + } + return nil +} +func (mgr *Manager) loadMetadata(ctx context.Context, img *pkgtypes.Image) (err error) { + cli := mgr.cli + resp, _, err := cli.ImageInspectWithRaw(ctx, mgr.dockerImageName(img)) + if err != nil { + return err + } + upperDir := resp.GraphDriver.Data["UpperDir"] + img.LocalPath = filepath.Join(upperDir, destImgName) + img.ActualSize, img.VirtualSize, err = utils.ImageSize(ctx, img.LocalPath) + + img.Digest = resp.Config.Labels["SHA256"] + return err +} + +func (mgr *Manager) dockerImageName(img *pkgtypes.Image) string { + cfg := mgr.cfg + if img.Username == "" { + return path.Join(cfg.Docker.Prefix, "library", img.Fullname()) + } else { //nolint + return path.Join(cfg.Docker.Prefix, img.Fullname()) + } +} + +func makeDockerClient(endpoint string) (*engineapi.Client, error) { + defaultHeaders := map[string]string{"User-Agent": "eru-yavirt"} + return engineapi.NewClientWithOpts( + engineapi.WithHost(endpoint), + engineapi.WithVersion(dockerCliVersion), + engineapi.WithHTTPClient(nil), + engineapi.WithHTTPHeaders(defaultHeaders)) +} + +func httpGetSHA256(ctx context.Context, u string) (string, error) { + if !strings.HasSuffix(u, ".img") { + return "", fmt.Errorf("invalid url: %s", u) + } + url := strings.TrimSuffix(u, ".img") + url += ".sha256sum" + // Perform GET request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // Read the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(body), nil +} diff --git a/pkg/vmimage/factory/factory.go b/pkg/vmimage/factory/factory.go new file mode 100644 index 0000000..835a222 --- /dev/null +++ b/pkg/vmimage/factory/factory.go @@ -0,0 +1,152 @@ +package factory + +import ( + "context" + "fmt" + "io" + + "github.com/alphadose/haxmap" + "github.com/projecteru2/yavirt/pkg/vmimage" + "github.com/projecteru2/yavirt/pkg/vmimage/docker" + "github.com/projecteru2/yavirt/pkg/vmimage/mocks" + "github.com/projecteru2/yavirt/pkg/vmimage/types" + "github.com/projecteru2/yavirt/pkg/vmimage/vmihub" +) + +var ( + gF *Factory +) + +func Setup(config *types.Config) (err error) { + gF, err = NewFactory(config) + return err +} + +type Factory struct { + cfg *types.Config + mgrMap *haxmap.Map[string, vmimage.Manager] +} + +func NewFactory(cfg *types.Config) (f *Factory, err error) { + f = &Factory{ + cfg: cfg, + mgrMap: haxmap.New[string, vmimage.Manager](), + } + + var mgr vmimage.Manager + switch cfg.Type { + case types.TypeDocker: + mgr, err = docker.NewManager(cfg) + case types.TypeVMIHub: + mgr, err = vmihub.NewManager(cfg) + case types.TypeMock: + mgr = &mocks.Manager{} + default: + err = fmt.Errorf("invalid type: %s", cfg.Type) + } + if err != nil { + return nil, err + } + f.mgrMap.Set(cfg.Type, mgr) + return f, nil +} + +func (f *Factory) GetManager(ty string) (mgr vmimage.Manager, err error) { + if ty == "" { + ty = f.cfg.Type + } + if mgr, _ = f.mgrMap.Get(ty); mgr != nil { + return mgr, nil + } + switch ty { + case types.TypeDocker: + mgr, err = docker.NewManager(f.cfg) + case types.TypeMock: + mgr = &mocks.Manager{} + default: + return nil, fmt.Errorf("invalid image manager type: %s", ty) + } + f.mgrMap.Set(ty, mgr) + return mgr, err +} + +func GetManager(tys ...string) (vmimage.Manager, error) { + ty := "" + if len(tys) > 0 { + ty = tys[0] + } + return gF.GetManager(ty) +} + +func LoadImage(ctx context.Context, imgName string) (img *types.Image, err error) { + mgr, err := GetManager() + if err != nil { + return nil, err + } + return mgr.LoadImage(ctx, imgName) +} + +func ListLocalImages(ctx context.Context, user string) ([]*types.Image, error) { + mgr, err := GetManager() + if err != nil { + return nil, err + } + return mgr.ListLocalImages(ctx, user) +} + +func Pull(ctx context.Context, img *types.Image, policy types.PullPolicy) (io.ReadCloser, error) { + mgr, err := GetManager() + if err != nil { + return nil, err + } + return mgr.Pull(ctx, img, policy) +} + +func Push(ctx context.Context, img *types.Image, force bool) (io.ReadCloser, error) { + mgr, err := GetManager() + if err != nil { + return nil, err + } + return mgr.Push(ctx, img, force) +} + +func Prepare(ctx context.Context, fname string, img *types.Image) (io.ReadCloser, error) { + mgr, err := GetManager() + if err != nil { + return nil, err + } + return mgr.Prepare(ctx, fname, img) +} + +func RemoveLocal(ctx context.Context, img *types.Image) error { + mgr, err := GetManager() + if err != nil { + return err + } + return mgr.RemoveLocal(ctx, img) +} + +func NewImage(imgName string) (*types.Image, error) { + return types.NewImage(imgName) +} + +func NewImageName(user, name string) string { + imgName := fmt.Sprintf("%s/%s", user, name) + if user == "" { + imgName = name + } + return imgName +} + +func GetMockManager() *mocks.Manager { + mgr, _ := GetManager(types.TypeMock) + return mgr.(*mocks.Manager) +} + +func CheckHealth(ctx context.Context) error { + mgr, err := GetManager() + if err != nil { + return err + } + return mgr.CheckHealth(ctx) +} diff --git a/pkg/vmimage/image.go b/pkg/vmimage/image.go new file mode 100644 index 0000000..2cf7c4f --- /dev/null +++ b/pkg/vmimage/image.go @@ -0,0 +1,19 @@ +package vmimage + +import ( + "context" + "io" + + "github.com/projecteru2/yavirt/pkg/vmimage/types" +) + +type Manager interface { + ListLocalImages(ctx context.Context, user string) ([]*types.Image, error) + LoadImage(ctx context.Context, imgName string) (*types.Image, error) // create image object and pull the image to local + + Prepare(ctx context.Context, fname string, img *types.Image) (io.ReadCloser, error) + Pull(ctx context.Context, img *types.Image, pullPolicy types.PullPolicy) (io.ReadCloser, error) + Push(ctx context.Context, img *types.Image, force bool) (io.ReadCloser, error) + RemoveLocal(ctx context.Context, img *types.Image) error + CheckHealth(ctx context.Context) error +} diff --git a/pkg/vmimage/mocks/Manager.go b/pkg/vmimage/mocks/Manager.go new file mode 100644 index 0000000..01dc97f --- /dev/null +++ b/pkg/vmimage/mocks/Manager.go @@ -0,0 +1,217 @@ +// Code generated by mockery v2.42.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + io "io" + + mock "github.com/stretchr/testify/mock" + + types "github.com/projecteru2/yavirt/pkg/vmimage/types" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// CheckHealth provides a mock function with given fields: ctx +func (_m *Manager) CheckHealth(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CheckHealth") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ListLocalImages provides a mock function with given fields: ctx, user +func (_m *Manager) ListLocalImages(ctx context.Context, user string) ([]*types.Image, error) { + ret := _m.Called(ctx, user) + + if len(ret) == 0 { + panic("no return value specified for ListLocalImages") + } + + var r0 []*types.Image + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]*types.Image, error)); ok { + return rf(ctx, user) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []*types.Image); ok { + r0 = rf(ctx, user) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*types.Image) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// LoadImage provides a mock function with given fields: ctx, imgName +func (_m *Manager) LoadImage(ctx context.Context, imgName string) (*types.Image, error) { + ret := _m.Called(ctx, imgName) + + if len(ret) == 0 { + panic("no return value specified for LoadImage") + } + + var r0 *types.Image + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*types.Image, error)); ok { + return rf(ctx, imgName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *types.Image); ok { + r0 = rf(ctx, imgName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Image) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, imgName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Prepare provides a mock function with given fields: ctx, fname, img +func (_m *Manager) Prepare(ctx context.Context, fname string, img *types.Image) (io.ReadCloser, error) { + ret := _m.Called(ctx, fname, img) + + if len(ret) == 0 { + panic("no return value specified for Prepare") + } + + var r0 io.ReadCloser + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *types.Image) (io.ReadCloser, error)); ok { + return rf(ctx, fname, img) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *types.Image) io.ReadCloser); ok { + r0 = rf(ctx, fname, img) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.ReadCloser) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *types.Image) error); ok { + r1 = rf(ctx, fname, img) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Pull provides a mock function with given fields: ctx, img, pullPolicy +func (_m *Manager) Pull(ctx context.Context, img *types.Image, pullPolicy types.PullPolicy) (io.ReadCloser, error) { + ret := _m.Called(ctx, img, pullPolicy) + + if len(ret) == 0 { + panic("no return value specified for Pull") + } + + var r0 io.ReadCloser + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Image, types.PullPolicy) (io.ReadCloser, error)); ok { + return rf(ctx, img, pullPolicy) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.Image, types.PullPolicy) io.ReadCloser); ok { + r0 = rf(ctx, img, pullPolicy) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.ReadCloser) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.Image, types.PullPolicy) error); ok { + r1 = rf(ctx, img, pullPolicy) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Push provides a mock function with given fields: ctx, img, force +func (_m *Manager) Push(ctx context.Context, img *types.Image, force bool) (io.ReadCloser, error) { + ret := _m.Called(ctx, img, force) + + if len(ret) == 0 { + panic("no return value specified for Push") + } + + var r0 io.ReadCloser + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Image, bool) (io.ReadCloser, error)); ok { + return rf(ctx, img, force) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.Image, bool) io.ReadCloser); ok { + r0 = rf(ctx, img, force) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.ReadCloser) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.Image, bool) error); ok { + r1 = rf(ctx, img, force) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// RemoveLocal provides a mock function with given fields: ctx, img +func (_m *Manager) RemoveLocal(ctx context.Context, img *types.Image) error { + ret := _m.Called(ctx, img) + + if len(ret) == 0 { + panic("no return value specified for RemoveLocal") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Image) error); ok { + r0 = rf(ctx, img) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewManager(t interface { + mock.TestingT + Cleanup(func()) +}) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/vmimage/types/config.go b/pkg/vmimage/types/config.go new file mode 100644 index 0000000..7b40c63 --- /dev/null +++ b/pkg/vmimage/types/config.go @@ -0,0 +1,64 @@ +package types + +import ( + "encoding/base64" + "encoding/json" + "net/url" + + "github.com/pkg/errors" +) + +type DockerConfig struct { + Endpoint string `toml:"endpoint" default:"unix:///var/run/docker.sock"` + Auth string `toml:"auth"` // in base64 + Prefix string `toml:"prefix"` + Username string `toml:"username"` + Password string `toml:"password"` +} + +type VMIHubConfig struct { + BaseDir string `toml:"base_dir"` + Addr string `toml:"addr"` + Username string `toml:"username"` + Password string `toml:"password"` +} + +type Config struct { + Type string `toml:"type" default:"docker"` + Docker DockerConfig `toml:"docker"` + VMIHub VMIHubConfig `toml:"vmihub"` +} + +func (cfg *Config) CheckAndRefine() error { + switch cfg.Type { + case TypeDocker: + if cfg.Docker.Username == "" || cfg.Docker.Password == "" { + return errors.New("docker's username or password should not be empty") + } + auth := map[string]string{ + "username": cfg.Docker.Username, + "password": cfg.Docker.Password, + } + authBytes, _ := json.Marshal(auth) + cfg.Docker.Auth = base64.StdEncoding.EncodeToString(authBytes) + case TypeVMIHub: + if cfg.VMIHub.Username == "" || cfg.VMIHub.Password == "" { + return errors.New("ImageHub's username or password should not be empty") + } + if cfg.VMIHub.Addr == "" { + return errors.New("ImageHub's address shouldn't be empty") + } + u, err := url.Parse(cfg.VMIHub.Addr) + if err != nil { + return errors.Wrapf(err, "failed to parse %s", cfg.VMIHub.Addr) + } + if u.Scheme == "" || u.Host == "" { + return errors.New("invalid image hub addr") + } + case TypeMock: + return nil + default: + return errors.New("unknown image hub type") + } + return nil +} diff --git a/pkg/vmimage/types/const.go b/pkg/vmimage/types/const.go new file mode 100644 index 0000000..0328b2e --- /dev/null +++ b/pkg/vmimage/types/const.go @@ -0,0 +1,7 @@ +package types + +const ( + TypeDocker = "docker" + TypeVMIHub = "vmihub" + TypeMock = "mock" +) diff --git a/pkg/vmimage/types/image.go b/pkg/vmimage/types/image.go new file mode 100644 index 0000000..50c423a --- /dev/null +++ b/pkg/vmimage/types/image.go @@ -0,0 +1,74 @@ +package types + +import ( + "fmt" + "strings" + + "github.com/projecteru2/yavirt/pkg/vmimage/utils" +) + +type PullPolicy string + +const ( + PullPolicyAlways = "Always" + PullPolicyIfNotPresent = "IfNotPresent" + PullPolicyNever = "Never" +) + +type OSInfo struct { + Type string `json:"type" default:"linux"` + Distrib string `json:"distrib" default:"ubuntu"` + Version string `json:"version"` + Arch string `json:"arch" default:"amd64"` +} + +type Image struct { + Username string `json:"username"` + Name string `json:"name"` + Tag string `json:"tag" description:"image tag, default:latest"` + Private bool `json:"private"` + OS OSInfo `json:"os" description:"operating system info"` + Size int64 `json:"size"` + Digest string `json:"digest" description:"image digest"` + Snapshot string `json:"snapshot" description:"image rbd snapshot"` + + ActualSize int64 + VirtualSize int64 + LocalPath string +} + +func NewImage(fullname string) (*Image, error) { + user, name, tag, err := utils.NormalizeImageName(fullname) + if err != nil { + return nil, err + } + return &Image{ + Username: user, + Name: name, + Tag: tag, + }, nil +} + +func (img *Image) Fullname() string { + if img.Username == "" { + return fmt.Sprintf("%s:%s", img.Name, img.Tag) + } else { //nolint + return fmt.Sprintf("%s/%s:%s", img.Username, img.Name, img.Tag) + } +} + +func (img *Image) RBDName() string { + name := strings.ReplaceAll(img.Fullname(), "/", ".") + return strings.ReplaceAll(name, ":", "-") +} + +func (img *Image) Filepath() string { + return img.LocalPath +} + +func (img *Image) GetDigest() string { + if img.Digest == "" { + img.Digest, _ = utils.CalcDigestOfFile(img.LocalPath) + } + return img.Digest +} diff --git a/pkg/vmimage/utils/image.go b/pkg/vmimage/utils/image.go new file mode 100644 index 0000000..6ad6196 --- /dev/null +++ b/pkg/vmimage/utils/image.go @@ -0,0 +1,83 @@ +package utils + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "strings" + + "github.com/pkg/errors" +) + +func NormalizeImageName(fullname string) (user, name, tag string, err error) { + var nameTag string + switch parts := strings.Split(fullname, "/"); len(parts) { + case 1: + nameTag = parts[0] + case 2: + user, nameTag = parts[0], parts[1] + default: + err = fmt.Errorf("invalid image name: %s", fullname) + return + } + + switch parts := strings.Split(nameTag, ":"); len(parts) { + case 1: + name, tag = parts[0], "latest" + case 2: + name, tag = parts[0], parts[1] + default: + err = fmt.Errorf("invalid image name: %s", fullname) + return + } + return +} + +func CalcDigestOfFile(fname string) (string, error) { + f, err := os.Open(fname) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + + _, err = io.Copy(h, f) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", h.Sum(nil)), nil +} + +// EnsureReaderClosed As the name says, +// blocks until the stream is empty, until we meet EOF +func EnsureReaderClosed(stream io.ReadCloser) error { + if stream == nil { + return nil + } + if _, err := io.Copy(io.Discard, stream); err != nil { + return errors.Wrap(err, "Empty stream failed") + } + return stream.Close() +} + +func ImageSize(_ context.Context, fname string) (int64, int64, error) { + cmds := []string{"qemu-img", "info", "--output=json", fname} + cmd := exec.Command(cmds[0], cmds[1:]...) //nolint + output, err := cmd.Output() + if err != nil { + return 0, 0, errors.Wrap(err, "failed to run qemu-img info") + } + res := map[string]any{} + err = json.Unmarshal(output, &res) + if err != nil { + return 0, 0, errors.Wrap(err, "output is not json") + } + virtualSize := res["virtual-size"] + actualSize := res["actual-size"] + return int64(actualSize.(float64)), int64(virtualSize.(float64)), nil +} diff --git a/pkg/vmimage/utils/image_test.go b/pkg/vmimage/utils/image_test.go new file mode 100644 index 0000000..a331d8f --- /dev/null +++ b/pkg/vmimage/utils/image_test.go @@ -0,0 +1,39 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalizeImageName(t *testing.T) { + tests := []struct { + fullname string + expectedUser string + expectedName string + expectedTag string + expectedError string + }{ + {"ubuntu", "", "ubuntu", "latest", ""}, + {"myuser/myimage:1.0", "myuser", "myimage", "1.0", ""}, + {"myimage:2.0", "", "myimage", "2.0", ""}, + {"invalid/image/name:tag:extra", "", "", "", "invalid image name: invalid/image/name:tag:extra"}, + {"invalid/image/name:tag", "", "", "", "invalid image name: invalid/image/name:tag"}, + {"image/name:tag:extra", "", "", "", "invalid image name: image/name:tag:extra"}, + } + + for _, test := range tests { + user, name, tag, err := NormalizeImageName(test.fullname) + + if test.expectedError == "" { + assert.Nil(t, err, "Error should be nil") + } else { + assert.EqualError(t, err, test.expectedError, "Error message should match") + continue + } + assert.Equal(t, test.expectedUser, user, "User should match") + assert.Equal(t, test.expectedName, name, "Name should match") + assert.Equal(t, test.expectedTag, tag, "Tag should match") + + } +} diff --git a/pkg/vmimage/utils/utils.go b/pkg/vmimage/utils/utils.go new file mode 100644 index 0000000..2f3e4ea --- /dev/null +++ b/pkg/vmimage/utils/utils.go @@ -0,0 +1,26 @@ +package utils + +import ( + "errors" + "time" + + probing "github.com/prometheus-community/pro-bing" +) + +func IPReachable(ip string, timeout time.Duration) error { + pinger, err := probing.NewPinger(ip) + if err != nil { + return err + } + pinger.Timeout = timeout + pinger.Count = 2 + err = pinger.Run() // Blocks until finished. + if err != nil { + return err + } + stats := pinger.Statistics() + if stats.PacketsRecv <= 0 { + return errors.New("unreachable") + } + return nil +} diff --git a/pkg/vmimage/vmihub/vmihub.go b/pkg/vmimage/vmihub/vmihub.go new file mode 100644 index 0000000..dfe4b81 --- /dev/null +++ b/pkg/vmimage/vmihub/vmihub.go @@ -0,0 +1,163 @@ +package vmihub + +import ( + "context" + "io" + "net/http" + "net/url" + + "github.com/pkg/errors" + imageAPI "github.com/projecteru2/vmihub/client/image" + apitypes "github.com/projecteru2/vmihub/client/types" + "github.com/projecteru2/yavirt/pkg/vmimage/types" +) + +type Manager struct { + api imageAPI.API + cfg *types.Config +} + +func NewManager(cfg *types.Config) (*Manager, error) { + cred := &apitypes.Credential{ + Username: cfg.VMIHub.Username, + Password: cfg.VMIHub.Password, + } + api, err := imageAPI.NewAPI(cfg.VMIHub.Addr, cfg.VMIHub.BaseDir, cred) + if err != nil { + return nil, err + } + return &Manager{ + api: api, + cfg: cfg, + }, nil +} + +func (mgr *Manager) ListLocalImages(_ context.Context, user string) ([]*types.Image, error) { + apiImages, err := mgr.api.ListLocalImages() + if err != nil { + return nil, err + } + ans := make([]*types.Image, 0, len(apiImages)) + for _, img := range apiImages { + if user != "" && user != img.Username { + continue + } + ans = append(ans, &types.Image{ + Username: img.Username, + Name: img.Name, + Tag: img.Tag, + Private: img.Private, + Size: img.Size, + Digest: img.Digest, + LocalPath: img.Filepath(), + }) + } + return ans, nil +} + +func (mgr *Manager) LoadImage(ctx context.Context, imgName string) (*types.Image, error) { + apiImage, err := mgr.api.GetInfo(ctx, imgName) + if err != nil { + return nil, err + } + img := &types.Image{ + Username: apiImage.Username, + Name: apiImage.Name, + Tag: apiImage.Tag, + Private: apiImage.Private, + Size: apiImage.Size, + Digest: apiImage.Digest, + OS: types.OSInfo{ + Type: apiImage.OS.Type, + Distrib: apiImage.OS.Distrib, + Version: apiImage.OS.Version, + Arch: apiImage.OS.Arch, + }, + Snapshot: apiImage.Snapshot, + } + return img, nil +} + +func (mgr *Manager) Prepare(_ context.Context, fname string, img *types.Image) (io.ReadCloser, error) { + apiImage, err := mgr.api.NewImage(img.Fullname()) + if err != nil { + return nil, err + } + err = apiImage.CopyFrom(fname) + return &nullReadCloser{}, err +} + +func (mgr *Manager) Pull(ctx context.Context, img *types.Image, policy types.PullPolicy) (io.ReadCloser, error) { + newImg, err := mgr.api.Pull(ctx, img.Fullname(), imageAPI.PullPolicy(policy)) + if err != nil { + return nil, err + } + img.Tag = newImg.Tag + img.Snapshot = newImg.Snapshot + img.OS = types.OSInfo{ + Type: newImg.OS.Type, + Distrib: newImg.OS.Distrib, + Version: newImg.OS.Version, + Arch: newImg.OS.Arch, + } + + return &nullReadCloser{}, nil +} + +func (mgr *Manager) Push(ctx context.Context, img *types.Image, force bool) (io.ReadCloser, error) { + apiImage := toAPIImage(img) + err := mgr.api.Push(ctx, apiImage, force) + return &nullReadCloser{}, err +} + +func (mgr *Manager) RemoveLocal(ctx context.Context, img *types.Image) error { + return mgr.api.RemoveLocalImage(ctx, toAPIImage(img)) +} + +func (mgr *Manager) CheckHealth(ctx context.Context) error { + healthzURL, err := url.JoinPath(mgr.cfg.VMIHub.Addr, "healthz") + if err != nil { + return err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, healthzURL, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.New(resp.Status) + } + return nil +} + +func toAPIImage(img *types.Image) *apitypes.Image { + apiImage := &apitypes.Image{} + apiImage.Username = img.Username + apiImage.Name = img.Name + apiImage.Tag = img.Tag + apiImage.Size = img.Size + apiImage.Digest = img.Digest + apiImage.OS = apitypes.OSInfo{ + Type: img.OS.Type, + Distrib: img.OS.Distrib, + Version: img.OS.Version, + Arch: img.OS.Arch, + } + return apiImage +} + +type nullReadCloser struct{} + +func (rc *nullReadCloser) Read([]byte) (int, error) { + return 0, io.EOF +} + +func (rc *nullReadCloser) Close() error { + return nil +}