From e30b4516151fd9b67063dfb40aff30d4d04e0827 Mon Sep 17 00:00:00 2001 From: Roman Mohr Date: Fri, 11 Mar 2022 13:49:52 +0100 Subject: [PATCH] Implement KubeVirt upload Signed-off-by: Roman Mohr --- docs/mantle/credentials.md | 5 ++ mantle/cmd/ore/kubevirt.go | 21 ++++++ mantle/cmd/ore/kubevirt/build.go | 104 +++++++++++++++++++++++++++++ mantle/cmd/ore/kubevirt/publish.go | 68 +++++++++++++++++++ mantle/cmd/ore/kubevirt/push.go | 15 +++++ src/cosalib/kubevirt.py | 37 ++++++++-- 6 files changed, 244 insertions(+), 6 deletions(-) create mode 100644 mantle/cmd/ore/kubevirt.go create mode 100644 mantle/cmd/ore/kubevirt/build.go create mode 100644 mantle/cmd/ore/kubevirt/publish.go create mode 100644 mantle/cmd/ore/kubevirt/push.go diff --git a/docs/mantle/credentials.md b/docs/mantle/credentials.md index 97ba018007..af7b7f8bd9 100644 --- a/docs/mantle/credentials.md +++ b/docs/mantle/credentials.md @@ -192,6 +192,11 @@ for more information about the `.boto` file. `qemu-unpriv` is run locally and needs no credentials. It has a restricted set of functionality compared to the `qemu` platform, such as: +## kubevirt + +`kubevirt` publishes a containerdisk which can be consumed by KubeVirt. In order to publish the conatinerdisk, the +credentials to the container registry need to be provided in `~/.docker/config.json`. + - No [Local cluster](platform/local/) - Usermode networking instead of namespaced networks * Single node only, no machine to machine networking diff --git a/mantle/cmd/ore/kubevirt.go b/mantle/cmd/ore/kubevirt.go new file mode 100644 index 0000000000..ee14d17064 --- /dev/null +++ b/mantle/cmd/ore/kubevirt.go @@ -0,0 +1,21 @@ +// Copyright 2021 Red Hat +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import "github.com/coreos/mantle/cmd/ore/kubevirt" + +func init() { + root.AddCommand(kubevirt.KubeVirt) +} diff --git a/mantle/cmd/ore/kubevirt/build.go b/mantle/cmd/ore/kubevirt/build.go new file mode 100644 index 0000000000..310241bb22 --- /dev/null +++ b/mantle/cmd/ore/kubevirt/build.go @@ -0,0 +1,104 @@ +package kubevirt + +import ( + "archive/tar" + "fmt" + "io" + "os" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +func BuildContainerDisk(imgPath string) (v1.Image, error) { + img := empty.Image + layerStream, errChan := StreamLayer(imgPath) + layer, err := tarball.LayerFromReader(layerStream) + if err != nil { + return nil, fmt.Errorf("error creating an image layer from disk: %v", err) + } + + img, err = mutate.AppendLayers(img, layer) + if err != nil { + return nil, fmt.Errorf("error appending the image layer: %v", err) + } + + if err := <-errChan; err != nil { + return nil, fmt.Errorf("error creating the tar file with the disk: %v", err) + } + return img, nil +} + +func StreamLayer(imagePath string) (tarReader io.ReadCloser, errChan chan error) { + errChan = make(chan error, 1) + reader, writer := io.Pipe() + tarWriter := tar.NewWriter(writer) + + go func() { + defer writer.Close() + defer tarWriter.Close() + err := addFileToTarWriter(imagePath, tarWriter) + if err != nil { + errChan <- fmt.Errorf("error adding file '%s', to tarball: %v", imagePath, err) + } + close(errChan) + }() + + return reader, errChan +} + +func addFileToTarWriter(filePath string, tarWriter *tar.Writer) error { + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("error opening file: %v", err) + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return fmt.Errorf("error getting file information with stat: %v", err) + } + + header := &tar.Header{ + Typeflag: tar.TypeDir, + Name: "disk/", + Mode: 0555, + Uid: 107, + Gid: 107, + Uname: "qemu", + Gname: "qemu", + ModTime: time.Now(), + } + + err = tarWriter.WriteHeader(header) + if err != nil { + return fmt.Errorf("error writing disks directory tar header: %v", err) + } + + header = &tar.Header{ + Typeflag: tar.TypeReg, + Uid: 107, + Gid: 107, + Uname: "qemu", + Gname: "qemu", + Name: "disk/disk.img", + Size: stat.Size(), + Mode: 0444, + ModTime: stat.ModTime(), + } + + err = tarWriter.WriteHeader(header) + if err != nil { + return fmt.Errorf("error writing image file tar header: %v", err) + } + + _, err = io.Copy(tarWriter, file) + if err != nil { + return fmt.Errorf("error writingfile into tarball: %v", err) + } + + return nil +} diff --git a/mantle/cmd/ore/kubevirt/publish.go b/mantle/cmd/ore/kubevirt/publish.go new file mode 100644 index 0000000000..bdb1fe21f6 --- /dev/null +++ b/mantle/cmd/ore/kubevirt/publish.go @@ -0,0 +1,68 @@ +package kubevirt + +import ( + "context" + "fmt" + "github.com/coreos/mantle/cli" + "github.com/coreos/pkg/capnslog" + "github.com/spf13/cobra" +) + +type Options struct { + Image string + Tags []string + Repository string + DryRun bool +} + +var ( + plog = capnslog.NewPackageLogger("github.com/coreos/mantle", "ore/kubevirt") + + KubeVirt = &cobra.Command{ + Use: "kubevirt", + Short: "kubevirt publish", + } + options = &Options{ + DryRun: true, + } + + publishCmd = &cobra.Command{ + Use: "publish", + RunE: func(cmd *cobra.Command, args []string) error { + return buildAndPublish(options) + }, + } +) + +func init() { + KubeVirt.AddCommand(publishCmd) + publishCmd.Flags().StringVar(&options.Image, "image", "", "Path to the qcow2 image to publish") + publishCmd.Flags().StringArrayVar(&options.Tags, "tag", options.Tags, "Container image tags") + publishCmd.Flags().StringVar(&options.Repository, "repository", options.Repository, "Container image repository") + publishCmd.Flags().BoolVar(&options.DryRun, "dry-run", options.DryRun, "dry run") + publishCmd.MarkFlagRequired("image") + publishCmd.MarkFlagRequired("tag") + publishCmd.MarkFlagRequired("repository") + cli.WrapPreRun(KubeVirt, preflightCheck) +} + +func buildAndPublish(options *Options) error { + plog.Infof("Building containerDisk for %s", options.Image) + containerDisk, err := BuildContainerDisk(options.Image) + if err != nil { + return fmt.Errorf("error creating the containerdisk : %v", err) + } + for _, tag := range options.Tags { + containerName := fmt.Sprintf("%s:%s", options.Repository, tag) + plog.Infof("Pushing containerDisk %q", containerName) + if !options.DryRun { + if err := PushImage(context.Background(), containerDisk, containerName); err != nil { + return err + } + } + } + return nil +} +func preflightCheck(cmd *cobra.Command, args []string) error { + return nil +} diff --git a/mantle/cmd/ore/kubevirt/push.go b/mantle/cmd/ore/kubevirt/push.go new file mode 100644 index 0000000000..0f9dd153b7 --- /dev/null +++ b/mantle/cmd/ore/kubevirt/push.go @@ -0,0 +1,15 @@ +package kubevirt + +import ( + "context" + "fmt" + "github.com/google/go-containerregistry/pkg/crane" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +func PushImage(ctx context.Context, img v1.Image, name string) error { + if err := crane.Push(img, name, crane.WithContext(ctx)); err != nil { + return fmt.Errorf("error pushing image %q: %v", img, err) + } + return nil +} diff --git a/src/cosalib/kubevirt.py b/src/cosalib/kubevirt.py index c1f5fca6ad..6599c6b220 100644 --- a/src/cosalib/kubevirt.py +++ b/src/cosalib/kubevirt.py @@ -1,7 +1,29 @@ +import os.path + +from cosalib.cmdlib import ( + run_verbose +) + + def kubevirt_run_ore(build, args): - print(""" -Images are not published to KubeVirt. This command is a placeholder. -""") + ore_args = ['ore'] + if args.log_level: + ore_args.extend(['--log-level', args.log_level]) + + name = f"{build.build_name}" + if args.name is not None: + name = args.name + tag = f"{build.build_id}-{build.basearch}" + full_name = os.path.join(args.repository, name) + ore_args.extend([ + 'kubevirt', 'publish', + '--image', f"{build.image_path}", + '--tag', tag, + '--repository', f"{full_name}", + ]) + if not args.dry_run: + ore_args.extend(['--dry-run', 'false']) + run_verbose(ore_args) def kubevirt_run_ore_replicate(*args, **kwargs): @@ -12,7 +34,10 @@ def kubevirt_run_ore_replicate(*args, **kwargs): def kubevirt_cli(parser): - """ - Extend a parser with the KubeVirt options - """ + parser.add_argument("--name", + help="Name to append to the repository (e.g. fedora-coreos). Defaults to the build name.") + parser.add_argument("--repository", help="repository to push to (e.g. quay.io or quay.io/myorg)", required=True) + parser.add_argument('--dry-run', dest='dry_run', action='store_true') + parser.add_argument('--no-dry-run', dest='dry_run', action='store_false') + parser.set_defaults(dry_run=True) return parser