Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specify image size #522

Merged
merged 2 commits into from
Nov 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/cdi-importer/importer.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func main() {
sec, _ := util.ParseEnvVar(common.ImporterSecretKey, false)
source, _ := util.ParseEnvVar(common.ImporterSource, false)
contentType, _ := util.ParseEnvVar(common.ImporterContentType, false)
imageSize, _ := util.ParseEnvVar(common.ImporterImageSize, false)

glog.V(1).Infoln("begin import process")
dso := &importer.DataStreamOptions{
Expand All @@ -53,6 +54,7 @@ func main() {
sec,
source,
contentType,
imageSize,
}
err = importer.CopyImage(dso)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const (
ImporterAccessKeyID = "IMPORTER_ACCESS_KEY_ID"
// ImporterSecretKey provides a constant to capture our env variable "IMPORTER_SECRET_KEY"
ImporterSecretKey = "IMPORTER_SECRET_KEY"
// ImporterImageSize provides a constant to capture our env variable "IMPORTER_IMAGE_SIZE"
ImporterImageSize = "IMPORTER_IMAGE_SIZE"

// OwnerUID provides the UID of the owner entity (either PVC or DV)
OwnerUID = "OWNER_UID"
Expand Down
6 changes: 5 additions & 1 deletion pkg/controller/import-controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type ImportController struct {
}

type importPodEnvVar struct {
ep, secretName, source, contentType string
ep, secretName, source, contentType, imageSize string
}

// NewImportController sets up an Import Controller, and returns a pointer to
Expand Down Expand Up @@ -119,6 +119,10 @@ func (ic *ImportController) processPvcItem(pvc *v1.PersistentVolumeClaim) error
if podEnvVar.secretName == "" {
glog.V(2).Infof("no secret will be supplied to endpoint %q\n", podEnvVar.ep)
}
podEnvVar.imageSize, err = getImageSize(pvc)
if err != nil {
return err
}
}
// all checks passed, let's create the importer pod!
ic.expectPodCreate(pvcKey)
Expand Down
8 changes: 8 additions & 0 deletions pkg/controller/import_controller_ginkgo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
. "github.com/onsi/gomega"
"github.com/pkg/errors"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
Expand Down Expand Up @@ -233,6 +234,13 @@ func createInMemPVC(ns, name string, annotations map[string]string) *v1.Persiste
Annotations: annotations,
UID: "1234",
},
Spec: v1.PersistentVolumeClaimSpec{
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceName(v1.ResourceStorage): resource.MustParse("1G"),
},
},
},
}
}

Expand Down
12 changes: 12 additions & 0 deletions pkg/controller/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ func getEndpoint(pvc *v1.PersistentVolumeClaim) (string, error) {
return ep, nil
}

func getImageSize(pvc *v1.PersistentVolumeClaim) (string, error) {
pvcSize, found := pvc.Spec.Resources.Requests[v1.ResourceStorage]
if !found {
return "", errors.Errorf("storage request is missing in pvc \"%s/%s\"", pvc.Namespace, pvc.Name)
}
return pvcSize.String(), nil
}

// returns the source string which determines the type of source. If no source or invalid source found, default to http
func getSource(pvc *v1.PersistentVolumeClaim) string {
source, found := pvc.Annotations[AnnSource]
Expand Down Expand Up @@ -329,6 +337,10 @@ func makeEnv(podEnvVar importPodEnvVar, uid types.UID) []v1.EnvVar {
Name: common.ImporterContentType,
Value: podEnvVar.contentType,
},
{
Name: common.ImporterImageSize,
Value: podEnvVar.imageSize,
},
{
Name: common.OwnerUID,
Value: string(uid),
Expand Down
67 changes: 62 additions & 5 deletions pkg/controller/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,43 @@ func Test_getContentType(t *testing.T) {
}
}

func Test_getImageSize(t *testing.T) {
type args struct {
pvc *v1.PersistentVolumeClaim
}

tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "expected to get size 1G",
args: args{createPvc("testPVC", "default", nil, nil)},
want: "1G",
wantErr: false,
},
{
name: "expected to get error, because of missing size",
args: args{createPvcNoSize("testPVC", "default", nil, nil)},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getImageSize(tt.args.pvc)
if err != nil && !tt.wantErr {
t.Errorf("Error retrieving adjusted image size, when not expecting error: %s", err.Error())
}
if got != tt.want {
t.Errorf("getSource() = %v, want %v", got, tt.want)
}
})
}
}

func Test_getSecretName(t *testing.T) {
type args struct {
client kubernetes.Interface
Expand Down Expand Up @@ -717,8 +754,8 @@ func TestCreateImporterPod(t *testing.T) {
}{
{
name: "expect pod to be created",
args: args{k8sfake.NewSimpleClientset(pvc), "test/image", "-v=5", "Always", importPodEnvVar{"", "", "", ""}, pvc},
want: MakeImporterPodSpec("test/image", "-v=5", "Always", importPodEnvVar{"", "", "", ""}, pvc),
args: args{k8sfake.NewSimpleClientset(pvc), "test/image", "-v=5", "Always", importPodEnvVar{"", "", "", "", "1G"}, pvc},
want: MakeImporterPodSpec("test/image", "-v=5", "Always", importPodEnvVar{"", "", "", "", "1G"}, pvc),
wantErr: false,
},
}
Expand Down Expand Up @@ -756,7 +793,7 @@ func TestMakeImporterPodSpec(t *testing.T) {
}{
{
name: "expect pod to be created",
args: args{"test/myimage", "5", "Always", importPodEnvVar{"", "", SourceHTTP, ContentTypeKubevirt}, pvc},
args: args{"test/myimage", "5", "Always", importPodEnvVar{"", "", SourceHTTP, ContentTypeKubevirt, "1G"}, pvc},
wantPod: pod,
},
}
Expand Down Expand Up @@ -786,8 +823,8 @@ func Test_makeEnv(t *testing.T) {
}{
{
name: "env should match",
args: args{importPodEnvVar{"myendpoint", "mysecret", SourceHTTP, ContentTypeKubevirt}},
want: createEnv(importPodEnvVar{"myendpoint", "mysecret", SourceHTTP, ContentTypeKubevirt}, mockUID),
args: args{importPodEnvVar{"myendpoint", "mysecret", SourceHTTP, ContentTypeKubevirt, "1G"}},
want: createEnv(importPodEnvVar{"myendpoint", "mysecret", SourceHTTP, ContentTypeKubevirt, "1G"}, mockUID),
},
}
for _, tt := range tests {
Expand Down Expand Up @@ -908,6 +945,7 @@ func createPod(pvc *v1.PersistentVolumeClaim, dvname string) *v1.Pod {
ep, _ := getEndpoint(pvc)
source := getSource(pvc)
contentType := getContentType(pvc)
imageSize, _ := getImageSize(pvc)
pod.Spec.Containers[0].Env = []v1.EnvVar{
{
Name: ImporterSource,
Expand All @@ -921,6 +959,10 @@ func createPod(pvc *v1.PersistentVolumeClaim, dvname string) *v1.Pod {
Name: ImporterContentType,
Value: contentType,
},
{
Name: ImporterImageSize,
Value: imageSize,
},
{
Name: OwnerUID,
Value: string(pvc.UID),
Expand Down Expand Up @@ -948,6 +990,17 @@ func createPvc(name, ns string, annotations, labels map[string]string) *v1.Persi
}
}

func createPvcNoSize(name, ns string, annotations, labels map[string]string) *v1.PersistentVolumeClaim {
return &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Annotations: annotations,
Labels: labels,
},
}
}

func createClonePvc(name, ns string, annotations, labels map[string]string) *v1.PersistentVolumeClaim {
return &v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -998,6 +1051,10 @@ func createEnv(podEnvVar importPodEnvVar, uid string) []v1.EnvVar {
Name: ImporterContentType,
Value: podEnvVar.contentType,
},
{
Name: ImporterImageSize,
Value: podEnvVar.imageSize,
},
{
Name: OwnerUID,
Value: string(uid),
Expand Down
39 changes: 39 additions & 0 deletions pkg/image/qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os"
"regexp"
"strconv"
"strings"

"github.com/golang/glog"
"github.com/pkg/errors"
Expand All @@ -41,10 +42,22 @@ const (
matcherString = "\\((\\d?\\d\\.\\d\\d)\\/100%\\)"
)

// ImgInfo contains the virtual image information.
type ImgInfo struct {
// Format contains the format of the image
Format string `json:"format"`
// BackingFile is the file name of the backing file
BackingFile string `json:"backing-filename"`
// VirtualSize is the virtual size of the image
VirtualSize int64 `json:"virtual-size"`
}

// QEMUOperations defines the interface for executing qemu subprocesses
type QEMUOperations interface {
ConvertQcow2ToRaw(string, string) error
ConvertQcow2ToRawStream(*url.URL, string) error
Resize(string, string) error
Info(string) (*ImgInfo, error)
Validate(string, string) error
}

Expand Down Expand Up @@ -98,6 +111,32 @@ func (o *qemuOperations) ConvertQcow2ToRawStream(url *url.URL, dest string) erro
return nil
}

func (o *qemuOperations) Resize(image, size string) error {
// size from k8s contains an upper case K, so we need to lower case it before we can pass it to qemu.
// Below is the message from qemu that explains what it expects.
// qemu-img: Parameter 'size' expects a non-negative number below 2^64 Optional suffix k, M, G, T, P or E means kilo-, mega-, giga-, tera-, peta-and exabytes, respectively.
size = strings.Replace(size, "K", "k", -1)
_, err := qemuExecFunction(qemuLimits, nil, "qemu-img", "resize", "-f", "raw", image, size)
if err != nil {
return errors.Wrapf(err, "Error resizing image %s", image)
}
return nil
}

func (o *qemuOperations) Info(image string) (*ImgInfo, error) {
output, err := qemuExecFunction(qemuLimits, nil, "qemu-img", "info", "--output=json", image)
if err != nil {
return nil, errors.Wrapf(err, "Error getting info on image %s", image)
}
var info ImgInfo
err = json.Unmarshal(output, &info)
if err != nil {
glog.Errorf("Invalid JSON:\n%s\n", string(output))
return nil, errors.Wrapf(err, "Invalid json for image %s", image)
}
return &info, nil
}

func (o *qemuOperations) Validate(image, format string) error {
type imageInfo struct {
Format string `json:"format"`
Expand Down
38 changes: 36 additions & 2 deletions pkg/importer/dataStream.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import (
"strings"
"time"

"k8s.io/apimachinery/pkg/api/resource"

"github.com/golang/glog"
"github.com/minio/minio-go"
"github.com/pkg/errors"
Expand Down Expand Up @@ -89,6 +91,8 @@ type DataStreamOptions struct {
Source string
// ContentType is the content type of the data.
ContentType string
// ImageSize is the size we want the resulting image to be.
ImageSize string
}

const (
Expand Down Expand Up @@ -128,6 +132,7 @@ func newDataStreamFromStream(stream io.ReadCloser) (*DataStream, error) {
"",
controller.SourceHTTP,
controller.ContentTypeKubevirt,
"", // Blank means don't resize
}, stream)
}

Expand Down Expand Up @@ -307,6 +312,29 @@ func SaveStream(stream io.ReadCloser, dest string) (int64, error) {
return ds.Size, nil
}

// ResizeImage resizes the images to match the requested size. Sometimes provisioners misbehave and the available space
// is not the same as the requested space. For those situations we compare the available space to the requested space and
// use the smallest of the two values.
func ResizeImage(dest, imageSize string) error {
Copy link
Contributor

@zvikorn zvikorn Nov 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@awels Is this being called somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, you are right it is not, but obviously it should, making a patch to fix now.

info, err := qemuOperations.Info(dest)
if err != nil {
return err
}
currentImageSizeQuantity := resource.NewQuantity(info.VirtualSize, resource.BinarySI)
newImageSizeQuantity := resource.MustParse(imageSize)
minSizeQuantity := util.MinQuantity(resource.NewScaledQuantity(util.GetAvailableSpace(dest), 0), &newImageSizeQuantity)
if minSizeQuantity.Cmp(newImageSizeQuantity) != 0 {
// Available dest space is smaller than the size we want to resize to
glog.Warningf("Available space less than requested size, resizing image to available space %s.\n", minSizeQuantity.String())
}
if currentImageSizeQuantity.Cmp(minSizeQuantity) == 0 {
glog.V(1).Infof("No need to resize image. Requested size: %s, Image size: %d.\n", imageSize, info.VirtualSize)
return nil
}
glog.V(1).Infof("Expanding image size to: %s\n", minSizeQuantity.String())
return qemuOperations.Resize(dest, minSizeQuantity.String())
}

// Read the endpoint and determine the file composition (eg. .iso.tar.gz) based on the magic number in
// each known file format header. Set the Reader slice in the receiver and set the Size field to each
// reader's original size. Note: if, when this method returns, the Size is still 0 then another method
Expand Down Expand Up @@ -623,11 +651,11 @@ func (d *DataStream) copy(dest string) error {

return nil
}
return copy(d.topReader(), dest, d.qemu)
return copy(d.topReader(), dest, d.qemu, d.ImageSize)
}

// Copy the file using its Reader (r) to the passed-in destination (`out`).
func copy(r io.Reader, out string, qemu bool) error {
func copy(r io.Reader, out string, qemu bool, imageSize string) error {
out = filepath.Clean(out)
glog.V(2).Infof("copying image file to %q", out)
dest := out
Expand Down Expand Up @@ -655,6 +683,12 @@ func copy(r io.Reader, out string, qemu bool) error {
if err != nil {
return errors.Wrap(err, "Local qcow to raw conversion failed")
}
if imageSize != "" {
err = qemuOperations.Resize(dest, imageSize)
}
if err != nil {
return errors.Wrap(err, "Resize of image failed")
}
}
return nil
}
Expand Down
Loading