Support registry import using node docker cache (#1913)

* Support registry import using node docker cache

The new CRI (container runtime interface) importer pod is created with three containers and a shared emptyDir volume:
-Init container: copies static http server binary to empty dir
-Server container: container image container configured to run the http binary and serve up the image file in /data
-Client container: import.sh uses cdi-import to import from server container, and writes "done" file on emptydir
-Server container sees "done" file and exits

Thanks mhenriks for the PoC!

Done:
-added ImportMethod to DataVolumeSourceRegistry (DataVolume.Spec.Source.Registry, DataImportCron.Spec.Source.Registry).
Import method can be "skopeo" (default), or "cri" for container runtime interface based import
-added cdi-containerimage-server & import.sh to the cdi-importer container

ToDo:
-utests and func tests
-doc

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>

* Add tests, fix CR comments

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>

* CR fixes

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>

* Use deployment docker prefix and tag in func tests

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>

* Add OpenShift ImageStreams import support

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>

* Add importer pod lookup annotation for image streams

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>

* Add pullMethod and imageStream doc

Signed-off-by: Arnon Gilboa <agilboa@redhat.com>
This commit is contained in:
Arnon Gilboa 2021-09-20 23:05:36 +03:00 committed by GitHub
parent 22afd303be
commit addf25b4f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 919 additions and 181 deletions

View File

@ -58,6 +58,7 @@ container_bundle(
"$(container_prefix)/vcenter-simulator:$(container_tag)": "//tools/vddk-test:vcenter-simulator", "$(container_prefix)/vcenter-simulator:$(container_tag)": "//tools/vddk-test:vcenter-simulator",
"$(container_prefix)/vddk-init:$(container_tag)": "//tools/vddk-init:vddk-init-image", "$(container_prefix)/vddk-init:$(container_tag)": "//tools/vddk-init:vddk-init-image",
"$(container_prefix)/vddk-test:$(container_tag)": "//tools/vddk-test:vddk-test-image", "$(container_prefix)/vddk-test:$(container_tag)": "//tools/vddk-test:vddk-test-image",
"$(container_prefix)/cdi-func-test-tinycore:$(container_tag)": "//tests:cdi-func-test-tinycore",
}, },
) )
@ -74,6 +75,7 @@ container_bundle(
"$(container_prefix)/imageio-init:$(container_tag)": "//tools/imageio-init:imageio-init-image", "$(container_prefix)/imageio-init:$(container_tag)": "//tools/imageio-init:imageio-init-image",
"$(container_prefix)/loop-back-lvm:$(container_tag)": "//tools/loop-back-lvm:loop-back-lvm-image", "$(container_prefix)/loop-back-lvm:$(container_tag)": "//tools/loop-back-lvm:loop-back-lvm-image",
"$(container_prefix)/vcenter-simulator:$(container_tag)": "//tools/vddk-test:vcenter-simulator", "$(container_prefix)/vcenter-simulator:$(container_tag)": "//tools/vddk-test:vcenter-simulator",
"$(container_prefix)/cdi-func-test-tinycore:$(container_tag)": "//tests:cdi-func-test-tinycore",
}, },
) )

View File

@ -3935,22 +3935,26 @@
"v1beta1.DataVolumeSourceRegistry": { "v1beta1.DataVolumeSourceRegistry": {
"description": "DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source", "description": "DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source",
"type": "object", "type": "object",
"required": [
"url"
],
"properties": { "properties": {
"certConfigMap": { "certConfigMap": {
"description": "CertConfigMap provides a reference to the Registry certs", "description": "CertConfigMap provides a reference to the Registry certs",
"type": "string" "type": "string"
}, },
"imageStream": {
"description": "ImageStream is the name of image stream for import",
"type": "string"
},
"pullMethod": {
"description": "PullMethod can be either \"pod\" (default import), or \"node\" (node docker cache based import)",
"type": "string"
},
"secretRef": { "secretRef": {
"description": "SecretRef provides the secret reference needed to access the Registry source", "description": "SecretRef provides the secret reference needed to access the Registry source",
"type": "string" "type": "string"
}, },
"url": { "url": {
"description": "URL is the url of the Docker registry source", "description": "URL is the url of the registry source (starting with the scheme: docker, oci-archive)",
"type": "string", "type": "string"
"default": ""
} }
} }
}, },

View File

@ -89,6 +89,7 @@ container_image(
], ],
files = [ files = [
":cdi-importer", ":cdi-importer",
"//tools/cdi-containerimage-server",
], ],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )

View File

@ -18,6 +18,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"strconv" "strconv"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -39,6 +40,31 @@ func init() {
flag.Parse() flag.Parse()
} }
func waitForReadyFile() {
readyFile, _ := util.ParseEnvVar(common.ImporterReadyFile, false)
if readyFile == "" {
return
}
for {
if _, err := os.Stat(readyFile); err == nil {
break
}
time.Sleep(time.Second)
}
}
func touchDoneFile() {
doneFile, _ := util.ParseEnvVar(common.ImporterDoneFile, false)
if doneFile == "" {
return
}
f, err := os.OpenFile(doneFile, os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
klog.Errorf("Failed creating file %s: %+v", doneFile, err)
}
f.Close()
}
func main() { func main() {
defer klog.Flush() defer klog.Flush()
@ -186,6 +212,7 @@ func main() {
} }
defer dp.Close() defer dp.Close()
processor := importer.NewDataProcessor(dp, dest, dataDir, common.ScratchDataDir, imageSize, filesystemOverhead, preallocation) processor := importer.NewDataProcessor(dp, dest, dataDir, common.ScratchDataDir, imageSize, filesystemOverhead, preallocation)
waitForReadyFile()
err = processor.ProcessData() err = processor.ProcessData()
if err != nil { if err != nil {
klog.Errorf("%+v", err) klog.Errorf("%+v", err)
@ -200,6 +227,7 @@ func main() {
dp.Close() dp.Close()
os.Exit(1) os.Exit(1)
} }
touchDoneFile()
preallocationApplied = processor.PreallocationApplied() preallocationApplied = processor.PreallocationApplied()
} }
message := "Import Complete" message := "Import Complete"

View File

@ -126,3 +126,47 @@ Add the registry to CDIConfig insecureRegistries in the `cdi` namespace.
```bash ```bash
kubectl patch cdi cdi --patch '{"spec": {"config": {"insecureRegistries": ["my-private-registry-host:5000"]}}}' --type merge kubectl patch cdi cdi --patch '{"spec": {"config": {"insecureRegistries": ["my-private-registry-host:5000"]}}}' --type merge
``` ```
# Import registry image into a Data volume using node docker cache
We also support import using `node pullMethod` which is based on the node docker cache. This is useful when registry image is usable via `Container.Image` but CDI importer is not authorized to access it (e.g. registry.redhat.io requires a pull secret):
```yaml
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
name: registry-image-datavolume
spec:
source:
registry:
url: "docker://kubevirt/cirros-container-disk-demo:devel"
pullMethod: node
pvc:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
```
Using this method we also support import from OpenShift `imageStream` instead of `url`:
```yaml
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
name: registry-image-datavolume
spec:
source:
registry:
imageStream: rhel8-guest-is
pullMethod: node
pvc:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
```
More information on image streams is available [here](https://docs.openshift.com/container-platform/4.8/openshift_images/image-streams-manage.html) and [here](https://www.tutorialworks.com/openshift-imagestreams).

View File

@ -41,6 +41,10 @@ BLOCK_SC=${BLOCK_SC:-rook-ceph-block}
# so on one SC we can test CSI clone and on the other the smartclone # so on one SC we can test CSI clone and on the other the smartclone
CSICLONE_SC=${CSICLONE_SC:-rook-ceph-block} CSICLONE_SC=${CSICLONE_SC:-rook-ceph-block}
OPERATOR_CONTAINER_IMAGE=$(./cluster-up/kubectl.sh get deployment -n $CDI_NAMESPACE cdi-operator -o'custom-columns=spec:spec.template.spec.containers[0].image' --no-headers)
DOCKER_PREFIX=${OPERATOR_CONTAINER_IMAGE%/*}
DOCKER_TAG=${OPERATOR_CONTAINER_IMAGE##*:}
if [ -z "${KUBECTL+x}" ]; then if [ -z "${KUBECTL+x}" ]; then
kubevirtci_kubectl="${BASE_PATH}/${KUBEVIRT_PROVIDER}/.kubectl" kubevirtci_kubectl="${BASE_PATH}/${KUBEVIRT_PROVIDER}/.kubectl"
if [ -e ${kubevirtci_kubectl} ]; then if [ -e ${kubevirtci_kubectl} ]; then
@ -65,8 +69,10 @@ arg_gocli="${GOCLI:+-gocli-path=$GOCLI}"
arg_sc_snap="${SNAPSHOT_SC:+-snapshot-sc=$SNAPSHOT_SC}" arg_sc_snap="${SNAPSHOT_SC:+-snapshot-sc=$SNAPSHOT_SC}"
arg_sc_block="${BLOCK_SC:+-block-sc=$BLOCK_SC}" arg_sc_block="${BLOCK_SC:+-block-sc=$BLOCK_SC}"
arg_sc_csi="${CSICLONE_SC:+-csiclone-sc=$CSICLONE_SC}" arg_sc_csi="${CSICLONE_SC:+-csiclone-sc=$CSICLONE_SC}"
arg_docker_prefix="${DOCKER_PREFIX:+-docker-prefix=$DOCKER_PREFIX}"
arg_docker_tag="${DOCKER_TAG:+-docker-tag=$DOCKER_TAG}"
test_args="${test_args} -ginkgo.v ${arg_master} ${arg_namespace} ${arg_kubeconfig} ${arg_kubectl} ${arg_oc} ${arg_gocli} ${arg_sc_snap} ${arg_sc_block} ${arg_sc_csi}" test_args="${test_args} -ginkgo.v ${arg_master} ${arg_namespace} ${arg_kubeconfig} ${arg_kubectl} ${arg_oc} ${arg_gocli} ${arg_sc_snap} ${arg_sc_block} ${arg_sc_csi} ${arg_docker_prefix} ${arg_docker_tag}"
echo 'Wait until all CDI Pods are ready' echo 'Wait until all CDI Pods are ready'
retry_counter=0 retry_counter=0

View File

@ -15714,8 +15714,21 @@ func schema_pkg_apis_core_v1beta1_DataVolumeSourceRegistry(ref common.ReferenceC
Properties: map[string]spec.Schema{ Properties: map[string]spec.Schema{
"url": { "url": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "URL is the url of the Docker registry source", Description: "URL is the url of the registry source (starting with the scheme: docker, oci-archive)",
Default: "", Type: []string{"string"},
Format: "",
},
},
"imageStream": {
SchemaProps: spec.SchemaProps{
Description: "ImageStream is the name of image stream for import",
Type: []string{"string"},
Format: "",
},
},
"pullMethod": {
SchemaProps: spec.SchemaProps{
Description: "PullMethod can be either \"pod\" (default import), or \"node\" (node docker cache based import)",
Type: []string{"string"}, Type: []string{"string"},
Format: "", Format: "",
}, },
@ -15735,7 +15748,6 @@ func schema_pkg_apis_core_v1beta1_DataVolumeSourceRegistry(ref common.ReferenceC
}, },
}, },
}, },
Required: []string{"url"},
}, },
}, },
} }

View File

@ -152,14 +152,40 @@ type DataVolumeSourceS3 struct {
// DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source // DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source
type DataVolumeSourceRegistry struct { type DataVolumeSourceRegistry struct {
//URL is the url of the Docker registry source //URL is the url of the registry source (starting with the scheme: docker, oci-archive)
URL string `json:"url"` // +optional
URL *string `json:"url,omitempty"`
//ImageStream is the name of image stream for import
// +optional
ImageStream *string `json:"imageStream,omitempty"`
//PullMethod can be either "pod" (default import), or "node" (node docker cache based import)
// +optional
PullMethod *RegistryPullMethod `json:"pullMethod,omitempty"`
//SecretRef provides the secret reference needed to access the Registry source //SecretRef provides the secret reference needed to access the Registry source
SecretRef string `json:"secretRef,omitempty"` // +optional
SecretRef *string `json:"secretRef,omitempty"`
//CertConfigMap provides a reference to the Registry certs //CertConfigMap provides a reference to the Registry certs
CertConfigMap string `json:"certConfigMap,omitempty"` // +optional
CertConfigMap *string `json:"certConfigMap,omitempty"`
} }
const (
// RegistrySchemeDocker is docker scheme prefix
RegistrySchemeDocker = "docker"
// RegistrySchemeOci is oci-archive scheme prefix
RegistrySchemeOci = "oci-archive"
)
// RegistryPullMethod represents the registry import pull method
type RegistryPullMethod string
const (
// RegistryPullPod is the standard import
RegistryPullPod RegistryPullMethod = "pod"
// RegistryPullNode is the node docker cache based import
RegistryPullNode RegistryPullMethod = "node"
)
// DataVolumeSourceHTTP can be either an http or https endpoint, with an optional basic auth user name and password, and an optional configmap containing additional CAs // DataVolumeSourceHTTP can be either an http or https endpoint, with an optional basic auth user name and password, and an optional configmap containing additional CAs
type DataVolumeSourceHTTP struct { type DataVolumeSourceHTTP struct {
// URL is the URL of the http(s) endpoint // URL is the URL of the http(s) endpoint

View File

@ -82,9 +82,11 @@ func (DataVolumeSourceS3) SwaggerDoc() map[string]string {
func (DataVolumeSourceRegistry) SwaggerDoc() map[string]string { func (DataVolumeSourceRegistry) SwaggerDoc() map[string]string {
return map[string]string{ return map[string]string{
"": "DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source", "": "DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source",
"url": "URL is the url of the Docker registry source", "url": "URL is the url of the registry source (starting with the scheme: docker, oci-archive)\n+optional",
"secretRef": "SecretRef provides the secret reference needed to access the Registry source", "imageStream": "ImageStream is the name of image stream for import\n+optional",
"certConfigMap": "CertConfigMap provides a reference to the Registry certs", "pullMethod": "PullMethod can be either \"pod\" (default import), or \"node\" (node docker cache based import)\n+optional",
"secretRef": "SecretRef provides the secret reference needed to access the Registry source\n+optional",
"certConfigMap": "CertConfigMap provides a reference to the Registry certs\n+optional",
} }
} }

View File

@ -468,7 +468,7 @@ func (in *DataImportCronSource) DeepCopyInto(out *DataImportCronSource) {
if in.Registry != nil { if in.Registry != nil {
in, out := &in.Registry, &out.Registry in, out := &in.Registry, &out.Registry
*out = new(DataVolumeSourceRegistry) *out = new(DataVolumeSourceRegistry)
**out = **in (*in).DeepCopyInto(*out)
} }
return return
} }
@ -808,7 +808,7 @@ func (in *DataVolumeSource) DeepCopyInto(out *DataVolumeSource) {
if in.Registry != nil { if in.Registry != nil {
in, out := &in.Registry, &out.Registry in, out := &in.Registry, &out.Registry
*out = new(DataVolumeSourceRegistry) *out = new(DataVolumeSourceRegistry)
**out = **in (*in).DeepCopyInto(*out)
} }
if in.PVC != nil { if in.PVC != nil {
in, out := &in.PVC, &out.PVC in, out := &in.PVC, &out.PVC
@ -920,6 +920,31 @@ func (in *DataVolumeSourceRef) DeepCopy() *DataVolumeSourceRef {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DataVolumeSourceRegistry) DeepCopyInto(out *DataVolumeSourceRegistry) { func (in *DataVolumeSourceRegistry) DeepCopyInto(out *DataVolumeSourceRegistry) {
*out = *in *out = *in
if in.URL != nil {
in, out := &in.URL, &out.URL
*out = new(string)
**out = **in
}
if in.ImageStream != nil {
in, out := &in.ImageStream, &out.ImageStream
*out = new(string)
**out = **in
}
if in.PullMethod != nil {
in, out := &in.PullMethod, &out.PullMethod
*out = new(RegistryPullMethod)
**out = **in
}
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(string)
**out = **in
}
if in.CertConfigMap != nil {
in, out := &in.CertConfigMap, &out.CertConfigMap
*out = new(string)
**out = **in
}
return return
} }

View File

@ -23,7 +23,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url" neturl "net/url"
"reflect" "reflect"
admissionv1 "k8s.io/api/admission/v1" admissionv1 "k8s.io/api/admission/v1"
@ -50,7 +50,7 @@ func validateSourceURL(sourceURL string) string {
if sourceURL == "" { if sourceURL == "" {
return "source URL is empty" return "source URL is empty"
} }
url, err := url.ParseRequestURI(sourceURL) url, err := neturl.ParseRequestURI(sourceURL)
if err != nil { if err != nil {
return fmt.Sprintf("Invalid source URL: %s", sourceURL) return fmt.Sprintf("Invalid source URL: %s", sourceURL)
} }
@ -240,14 +240,73 @@ func (wh *dataVolumeValidatingWebhook) validateDataVolumeSpec(request *admission
return causes return causes
} }
if spec.Source.Registry != nil && spec.ContentType != "" && string(spec.ContentType) != string(cdiv1.DataVolumeKubeVirt) { if spec.Source.Registry != nil {
sourceType = field.Child("contentType").String() if spec.ContentType != "" && string(spec.ContentType) != string(cdiv1.DataVolumeKubeVirt) {
causes = append(causes, metav1.StatusCause{ sourceType = field.Child("contentType").String()
Type: metav1.CauseTypeFieldValueInvalid, causes = append(causes, metav1.StatusCause{
Message: fmt.Sprintf("ContentType must be " + string(cdiv1.DataVolumeKubeVirt) + " when Source is Registry"), Type: metav1.CauseTypeFieldValueInvalid,
Field: sourceType, Message: fmt.Sprintf("ContentType must be %s when Source is Registry", cdiv1.DataVolumeKubeVirt),
}) Field: sourceType,
return causes })
return causes
}
sourceURL := spec.Source.Registry.URL
sourceIS := spec.Source.Registry.ImageStream
if (sourceURL == nil && sourceIS == nil) || (sourceURL != nil && sourceIS != nil) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("Source registry should have either URL or ImageStream"),
Field: field.Child("source", "Registry").String(),
})
return causes
}
if sourceURL != nil {
url, err := neturl.Parse(*sourceURL)
if err != nil {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("Illegal registry source URL %s", *sourceURL),
Field: field.Child("source", "Registry", "URL").String(),
})
return causes
}
scheme := url.Scheme
if scheme != cdiv1.RegistrySchemeDocker && scheme != cdiv1.RegistrySchemeOci {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("Illegal registry source URL scheme %s", url),
Field: field.Child("source", "Registry", "URL").String(),
})
return causes
}
}
importMethod := spec.Source.Registry.PullMethod
if importMethod != nil && *importMethod != cdiv1.RegistryPullPod && *importMethod != cdiv1.RegistryPullNode {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("ImportMethod %s is neither %s, %s or \"\"", *importMethod, cdiv1.RegistryPullPod, cdiv1.RegistryPullNode),
Field: field.Child("source", "Registry", "importMethod").String(),
})
return causes
}
if sourceIS != nil && *sourceIS == "" {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("Source registry ImageStream is not valid"),
Field: field.Child("source", "Registry", "importMethod").String(),
})
return causes
}
if sourceIS != nil && (importMethod == nil || *importMethod != cdiv1.RegistryPullNode) {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Message: fmt.Sprintf("Source registry ImageStream is supported only with node pull import method"),
Field: field.Child("source", "Registry", "importMethod").String(),
})
return causes
}
} }
if spec.Source.Imageio != nil { if spec.Source.Imageio != nil {

View File

@ -68,12 +68,90 @@ var _ = Describe("Validating Webhook", func() {
Expect(resp.Allowed).To(Equal(false)) Expect(resp.Allowed).To(Equal(false))
}) })
It("should accept DataVolume with Registry source on create", func() { It("should accept DataVolume with Registry source URL on create", func() {
dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test") dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test")
resp := validateDataVolumeCreate(dataVolume) resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true)) Expect(resp.Allowed).To(Equal(true))
}) })
It("should accept DataVolume with Registry source ImageStream and node PullMethod on create", func() {
imageStream := "istream"
pullNode := cdiv1.RegistryPullNode
registrySource := cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{ImageStream: &imageStream, PullMethod: &pullNode},
}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolume("testDV", registrySource, pvc)
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should reject DataVolume with Registry source ImageStream and pod PullMethod on create", func() {
imageStream := "istream"
registrySource := cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{ImageStream: &imageStream},
}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolume("testDV", registrySource, pvc)
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with no url or ImageStream", func() {
registrySource := cdiv1.DataVolumeSource{}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolume("testDV", registrySource, pvc)
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with both url and ImageStream", func() {
url := "docker://registry:5000/test"
imageStream := "istream"
registrySource := cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{URL: &url, ImageStream: &imageStream},
}
pvc := newPVCSpec(pvcSizeDefault)
dataVolume := newDataVolume("testDV", registrySource, pvc)
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with non-kubevirt contentType", func() {
dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test")
dataVolume.Spec.ContentType = cdiv1.DataVolumeArchive
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with illegal source URL", func() {
dataVolume := newRegistryDataVolume("testDV", "docker/::registry:5000/test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with illegal transport in source URL", func() {
dataVolume := newRegistryDataVolume("testDV", "joker://registry:5000/test")
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should reject DataVolume with Registry source on create with illegal importMethod", func() {
pullMethod := cdiv1.RegistryPullMethod("nosuch")
dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test")
dataVolume.Spec.Source.Registry.PullMethod = &pullMethod
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(false))
})
It("should accept DataVolume with Registry source on create with supported importMethod", func() {
pullMethod := cdiv1.RegistryPullNode
dataVolume := newRegistryDataVolume("testDV", "docker://registry:5000/test")
dataVolume.Spec.Source.Registry.PullMethod = &pullMethod
resp := validateDataVolumeCreate(dataVolume)
Expect(resp.Allowed).To(Equal(true))
})
It("should accept DataVolume with PVC source on create", func() { It("should accept DataVolume with PVC source on create", func() {
dataVolume := newPVCDataVolume("testDV", "testNamespace", "test") dataVolume := newPVCDataVolume("testDV", "testNamespace", "test")
pvc := &corev1.PersistentVolumeClaim{ pvc := &corev1.PersistentVolumeClaim{
@ -520,7 +598,7 @@ func newHTTPDataVolume(name, url string) *cdiv1.DataVolume {
func newRegistryDataVolume(name, url string) *cdiv1.DataVolume { func newRegistryDataVolume(name, url string) *cdiv1.DataVolume {
registrySource := cdiv1.DataVolumeSource{ registrySource := cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{URL: url}, Registry: &cdiv1.DataVolumeSourceRegistry{URL: &url},
} }
pvc := newPVCSpec(pvcSizeDefault) pvc := newPVCSpec(pvcSizeDefault)
return newDataVolume(name, registrySource, pvc) return newDataVolume(name, registrySource, pvc)

View File

@ -88,6 +88,10 @@ const (
ImporterDiskID = "IMPORTER_DISK_ID" ImporterDiskID = "IMPORTER_DISK_ID"
// ImporterUUID provides a constant to capture our env variable "IMPORTER_UUID" // ImporterUUID provides a constant to capture our env variable "IMPORTER_UUID"
ImporterUUID = "IMPORTER_UUID" ImporterUUID = "IMPORTER_UUID"
// ImporterReadyFile provides a constant to capture our env variable "IMPORTER_READY_FILE"
ImporterReadyFile = "IMPORTER_READY_FILE"
// ImporterDoneFile provides a constant to capture our env variable "IMPORTER_DONE_FILE"
ImporterDoneFile = "IMPORTER_DONE_FILE"
// ImporterBackingFile provides a constant to capture our env variable "IMPORTER_BACKING_FILE" // ImporterBackingFile provides a constant to capture our env variable "IMPORTER_BACKING_FILE"
ImporterBackingFile = "IMPORTER_BACKING_FILE" ImporterBackingFile = "IMPORTER_BACKING_FILE"
// ImporterThumbprint provides a constant to capture our env variable "IMPORTER_THUMBPRINT" // ImporterThumbprint provides a constant to capture our env variable "IMPORTER_THUMBPRINT"

View File

@ -2287,13 +2287,28 @@ func (r *DatavolumeReconciler) newPersistentVolumeClaim(dataVolume *cdiv1.DataVo
} }
} else if dataVolume.Spec.Source.Registry != nil { } else if dataVolume.Spec.Source.Registry != nil {
annotations[AnnSource] = SourceRegistry annotations[AnnSource] = SourceRegistry
annotations[AnnEndpoint] = dataVolume.Spec.Source.Registry.URL pullMethod := dataVolume.Spec.Source.Registry.PullMethod
annotations[AnnContentType] = string(dataVolume.Spec.ContentType) if pullMethod != nil && *pullMethod != "" {
if dataVolume.Spec.Source.Registry.SecretRef != "" { annotations[AnnRegistryImportMethod] = string(*pullMethod)
annotations[AnnSecret] = dataVolume.Spec.Source.Registry.SecretRef
} }
if dataVolume.Spec.Source.Registry.CertConfigMap != "" { url := dataVolume.Spec.Source.Registry.URL
annotations[AnnCertConfigMap] = dataVolume.Spec.Source.Registry.CertConfigMap if url != nil && *url != "" {
annotations[AnnEndpoint] = *url
} else {
imageStream := dataVolume.Spec.Source.Registry.ImageStream
if imageStream != nil && *imageStream != "" {
annotations[AnnEndpoint] = *imageStream
annotations[AnnRegistryImageStream] = "true"
}
}
annotations[AnnContentType] = string(dataVolume.Spec.ContentType)
secretRef := dataVolume.Spec.Source.Registry.SecretRef
if secretRef != nil && *secretRef != "" {
annotations[AnnSecret] = *secretRef
}
certConfigMap := dataVolume.Spec.Source.Registry.CertConfigMap
if certConfigMap != nil && *certConfigMap != "" {
annotations[AnnCertConfigMap] = *certConfigMap
} }
} else if dataVolume.Spec.Source.PVC != nil { } else if dataVolume.Spec.Source.PVC != nil {
sourceNamespace := dataVolume.Spec.Source.PVC.Namespace sourceNamespace := dataVolume.Spec.Source.PVC.Namespace

View File

@ -8,8 +8,6 @@ import (
"strconv" "strconv"
"time" "time"
sdkapi "kubevirt.io/controller-lifecycle-operator-sdk/pkg/sdk/api"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/pkg/errors" "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -31,6 +29,7 @@ import (
featuregates "kubevirt.io/containerized-data-importer/pkg/feature-gates" featuregates "kubevirt.io/containerized-data-importer/pkg/feature-gates"
"kubevirt.io/containerized-data-importer/pkg/util" "kubevirt.io/containerized-data-importer/pkg/util"
"kubevirt.io/containerized-data-importer/pkg/util/naming" "kubevirt.io/containerized-data-importer/pkg/util/naming"
sdkapi "kubevirt.io/controller-lifecycle-operator-sdk/pkg/sdk/api"
) )
const ( const (
@ -61,6 +60,10 @@ const (
AnnCertConfigMap = AnnAPIGroup + "/storage.import.certConfigMap" AnnCertConfigMap = AnnAPIGroup + "/storage.import.certConfigMap"
// AnnContentType provides a const for the PVC content-type // AnnContentType provides a const for the PVC content-type
AnnContentType = AnnAPIGroup + "/storage.contentType" AnnContentType = AnnAPIGroup + "/storage.contentType"
// AnnRegistryImportMethod provides a const for registry import method annotation
AnnRegistryImportMethod = AnnAPIGroup + "/storage.import.registryImportMethod"
// AnnRegistryImageStream provides a const for registry image stream annotation
AnnRegistryImageStream = AnnAPIGroup + "/storage.import.registryImageStream"
// AnnImportPod provides a const for our PVC importPodName annotation // AnnImportPod provides a const for our PVC importPodName annotation
AnnImportPod = AnnAPIGroup + "/storage.import.importPodName" AnnImportPod = AnnAPIGroup + "/storage.import.importPodName"
// AnnRequiresScratch provides a const for our PVC requires scratch annotation // AnnRequiresScratch provides a const for our PVC requires scratch annotation
@ -81,6 +84,9 @@ const (
//AnnDefaultStorageClass is the annotation indicating that a storage class is the default one. //AnnDefaultStorageClass is the annotation indicating that a storage class is the default one.
AnnDefaultStorageClass = "storageclass.kubernetes.io/is-default-class" AnnDefaultStorageClass = "storageclass.kubernetes.io/is-default-class"
// AnnOpenShiftImageLookup is the annotation for OpenShift image stream lookup
AnnOpenShiftImageLookup = "alpha.image.policy.openshift.io/resolve-names"
// ErrImportFailedPVC provides a const to indicate an import to the PVC failed // ErrImportFailedPVC provides a const to indicate an import to the PVC failed
ErrImportFailedPVC = "ErrImportFailed" ErrImportFailedPVC = "ErrImportFailed"
// ImportSucceededPVC provides a const to indicate an import to the PVC failed // ImportSucceededPVC provides a const to indicate an import to the PVC failed
@ -91,6 +97,10 @@ const (
// ImportTargetInUse is reason for event created when an import pvc is in use // ImportTargetInUse is reason for event created when an import pvc is in use
ImportTargetInUse = "ImportTargetInUse" ImportTargetInUse = "ImportTargetInUse"
// importPodImageStreamFinalizer ensures image stream import pod is deleted when pvc is deleted,
// as in this case pod has no pvc OwnerReference
importPodImageStreamFinalizer = "cdi.kubevirt.io/importImageStream"
) )
// ImportReconciler members // ImportReconciler members
@ -117,6 +127,8 @@ type importPodEnvVar struct {
certConfigMap string certConfigMap string
diskID string diskID string
uuid string uuid string
readyFile string
doneFile string
backingFile string backingFile string
thumbprint string thumbprint string
filesystemOverhead string filesystemOverhead string
@ -131,6 +143,20 @@ type importPodEnvVar struct {
certConfigMapProxy string certConfigMapProxy string
} }
type importerPodArgs struct {
image string
importImage string
verbose string
pullPolicy string
podEnvVar *importPodEnvVar
pvc *corev1.PersistentVolumeClaim
scratchPvcName *string
podResourceRequirements *corev1.ResourceRequirements
workloadNodePlacement *sdkapi.NodePlacement
vddkImageName *string
priorityClassName string
}
// NewImportController creates a new instance of the import controller. // NewImportController creates a new instance of the import controller.
func NewImportController(mgr manager.Manager, log logr.Logger, importerImage, pullPolicy, verbose string, installerLabels map[string]string) (controller.Controller, error) { func NewImportController(mgr manager.Manager, log logr.Logger, importerImage, pullPolicy, verbose string, installerLabels map[string]string) (controller.Controller, error) {
uncachedClient, err := client.New(mgr.GetConfig(), client.Options{ uncachedClient, err := client.New(mgr.GetConfig(), client.Options{
@ -196,6 +222,10 @@ func isPVCComplete(pvc *corev1.PersistentVolumeClaim) bool {
return exists && (phase == string(corev1.PodSucceeded)) return exists && (phase == string(corev1.PodSucceeded))
} }
func isImageStream(pvc *corev1.PersistentVolumeClaim) bool {
return pvc.Annotations[AnnRegistryImageStream] == "true"
}
// Reconcile the reconcile loop for the CDIConfig object. // Reconcile the reconcile loop for the CDIConfig object.
func (r *ImportReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) { func (r *ImportReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) {
log := r.log.WithValues("PVC", req.NamespacedName) log := r.log.WithValues("PVC", req.NamespacedName)
@ -249,11 +279,9 @@ func (r *ImportReconciler) findImporterPod(pvc *corev1.PersistentVolumeClaim, lo
} }
return nil, nil return nil, nil
} }
if !metav1.IsControlledBy(pod, pvc) && !isImageStream(pvc) {
if !metav1.IsControlledBy(pod, pvc) {
return nil, errors.Errorf("Pod is not owned by PVC") return nil, errors.Errorf("Pod is not owned by PVC")
} }
log.V(1).Info("Pod is owned by PVC", pod.Name, pvc.Name) log.V(1).Info("Pod is owned by PVC", pod.Name, pvc.Name)
return pod, nil return pod, nil
} }
@ -301,7 +329,7 @@ func (r *ImportReconciler) reconcilePvc(pvc *corev1.PersistentVolumeClaim, log l
} else { } else {
if pvc.DeletionTimestamp != nil { if pvc.DeletionTimestamp != nil {
log.V(1).Info("PVC being terminated, delete pods", "pod.Name", pod.Name) log.V(1).Info("PVC being terminated, delete pods", "pod.Name", pod.Name)
if err := r.client.Delete(context.TODO(), pod); IgnoreNotFound(err) != nil { if err := r.cleanup(pvc, pod, log); err != nil {
return reconcile.Result{}, err return reconcile.Result{}, err
} }
} else { } else {
@ -411,7 +439,7 @@ func (r *ImportReconciler) updatePvcFromPod(pvc *corev1.PersistentVolumeClaim, p
} }
if shouldDeletePod(pvc) { if shouldDeletePod(pvc) {
log.V(1).Info("Deleting pod", "pod.Name", pod.Name) log.V(1).Info("Deleting pod", "pod.Name", pod.Name)
if err := r.client.Delete(context.TODO(), pod); IgnoreNotFound(err) != nil { if err := r.cleanup(pvc, pod, log); err != nil {
return err return err
} }
} }
@ -419,6 +447,19 @@ func (r *ImportReconciler) updatePvcFromPod(pvc *corev1.PersistentVolumeClaim, p
return nil return nil
} }
func (r *ImportReconciler) cleanup(pvc *corev1.PersistentVolumeClaim, pod *corev1.Pod, log logr.Logger) error {
if err := r.client.Delete(context.TODO(), pod); IgnoreNotFound(err) != nil {
return err
}
if HasFinalizer(pvc, importPodImageStreamFinalizer) {
RemoveFinalizer(pvc, importPodImageStreamFinalizer)
if err := r.updatePVC(pvc, log); err != nil {
return err
}
}
return nil
}
func (r *ImportReconciler) updatePVC(pvc *corev1.PersistentVolumeClaim, log logr.Logger) error { func (r *ImportReconciler) updatePVC(pvc *corev1.PersistentVolumeClaim, log logr.Logger) error {
log.V(1).Info("Annotations are now", "pvc.anno", pvc.GetAnnotations()) log.V(1).Info("Annotations are now", "pvc.anno", pvc.GetAnnotations())
if err := r.client.Update(context.TODO(), pvc); err != nil { if err := r.client.Update(context.TODO(), pvc); err != nil {
@ -459,12 +500,31 @@ func (r *ImportReconciler) createImporterPod(pvc *corev1.PersistentVolumeClaim)
return err return err
} }
// all checks passed, let's create the importer pod! // all checks passed, let's create the importer pod!
pod, err := createImporterPod(r.log, r.client, r.image, r.verbose, r.pullPolicy, podEnvVar, pvc, scratchPvcName, vddkImageName, getPriorityClass(pvc), r.installerLabels) podArgs := &importerPodArgs{
image: r.image,
verbose: r.verbose,
pullPolicy: r.pullPolicy,
podEnvVar: podEnvVar,
pvc: pvc,
scratchPvcName: scratchPvcName,
vddkImageName: vddkImageName,
priorityClassName: getPriorityClass(pvc),
}
pod, err := createImporterPod(r.log, r.client, podArgs, r.installerLabels)
if err != nil { if err != nil {
return err return err
} }
r.log.V(1).Info("Created POD", "pod.Name", pod.Name) r.log.V(1).Info("Created POD", "pod.Name", pod.Name)
// If importing from image stream, add finalizer. Note we don't watch the importer pod in this case,
// so to prevent a deadlock we add finalizer only if the pod is not retained after completion.
if isImageStream(pvc) && pvc.GetAnnotations()[AnnPodRetainAfterCompletion] != "true" {
AddFinalizer(pvc, importPodImageStreamFinalizer)
if err := r.updatePVC(pvc, r.log); err != nil {
return err
}
}
if requiresScratch { if requiresScratch {
r.log.V(1).Info("Pod requires scratch space") r.log.V(1).Info("Pod requires scratch space")
return r.createScratchPvcForPod(pvc, pod) return r.createScratchPvcForPod(pvc, pod)
@ -638,7 +698,7 @@ func (r *ImportReconciler) requiresScratchSpace(pvc *corev1.PersistentVolumeClai
case SourceGlance: case SourceGlance:
scratchRequired = true scratchRequired = true
case SourceRegistry: case SourceRegistry:
scratchRequired = true scratchRequired = pvc.Annotations[AnnRegistryImportMethod] != string(cdiv1.RegistryPullNode)
} }
} }
value, ok := pvc.Annotations[AnnRequiresScratch] value, ok := pvc.Annotations[AnnRequiresScratch]
@ -748,6 +808,22 @@ func getEndpoint(pvc *corev1.PersistentVolumeClaim) (string, error) {
return ep, nil return ep, nil
} }
// returns the import image part of the endpoint string
func getRegistryImportImage(pvc *corev1.PersistentVolumeClaim) (string, error) {
ep, err := getEndpoint(pvc)
if err != nil {
return "", nil
}
if isImageStream(pvc) {
return ep, nil
}
url, err := url.Parse(ep)
if err != nil {
return "", errors.Errorf("illegal registry endpoint %s", ep)
}
return url.Host + url.Path, nil
}
// getValueFromAnnotation returns the value of an annotation // getValueFromAnnotation returns the value of an annotation
func getValueFromAnnotation(pvc *corev1.PersistentVolumeClaim, annotation string) string { func getValueFromAnnotation(pvc *corev1.PersistentVolumeClaim, annotation string) string {
value, _ := pvc.Annotations[annotation] value, _ := pvc.Annotations[annotation]
@ -771,31 +847,162 @@ func createImportPodNameFromPvc(pvc *corev1.PersistentVolumeClaim) string {
// createImporterPod creates and returns a pointer to a pod which is created based on the passed-in endpoint, secret // createImporterPod creates and returns a pointer to a pod which is created based on the passed-in endpoint, secret
// name, and pvc. A nil secret means the endpoint credentials are not passed to the // name, and pvc. A nil secret means the endpoint credentials are not passed to the
// importer pod. // importer pod.
func createImporterPod(log logr.Logger, client client.Client, image, verbose, pullPolicy string, podEnvVar *importPodEnvVar, pvc *corev1.PersistentVolumeClaim, scratchPvcName *string, vddkImageName *string, priorityClassName string, installerLabels map[string]string) (*corev1.Pod, error) { func createImporterPod(log logr.Logger, client client.Client, args *importerPodArgs, installerLabels map[string]string) (*corev1.Pod, error) {
podResourceRequirements, err := GetDefaultPodResourceRequirements(client) var err error
args.podResourceRequirements, err = GetDefaultPodResourceRequirements(client)
if err != nil { if err != nil {
return nil, err return nil, err
} }
workloadNodePlacement, err := GetWorkloadNodePlacement(client) args.workloadNodePlacement, err = GetWorkloadNodePlacement(client)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pod := makeImporterPodSpec(pvc.Namespace, image, verbose, pullPolicy, podEnvVar, pvc, scratchPvcName, podResourceRequirements, workloadNodePlacement, vddkImageName, priorityClassName) var pod *corev1.Pod
if getSource(args.pvc) == SourceRegistry && args.pvc.Annotations[AnnRegistryImportMethod] == string(cdiv1.RegistryPullNode) {
args.importImage, err = getRegistryImportImage(args.pvc)
if err != nil {
return nil, err
}
pod = makeNodeImporterPodSpec(args)
} else {
pod = makeImporterPodSpec(args)
}
util.SetRecommendedLabels(pod, installerLabels, "cdi-controller") util.SetRecommendedLabels(pod, installerLabels, "cdi-controller")
if err := client.Create(context.TODO(), pod); err != nil { if err = client.Create(context.TODO(), pod); err != nil {
return nil, err return nil, err
} }
log.V(3).Info("importer pod created\n", "pod.Name", pod.Name, "pod.Namespace", pod.Namespace, "image name", image)
log.V(3).Info("importer pod created\n", "pod.Name", pod.Name, "pod.Namespace", pod.Namespace, "image name", args.image)
return pod, nil return pod, nil
} }
// makeImporterPodSpec creates and return the importer pod spec based on the passed-in endpoint, secret and pvc. // makeNodeImporterPodSpec creates and returns the node docker cache based importer pod spec based on the passed-in importImage and pvc.
func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar *importPodEnvVar, pvc *corev1.PersistentVolumeClaim, scratchPvcName *string, podResourceRequirements *corev1.ResourceRequirements, workloadNodePlacement *sdkapi.NodePlacement, vddkImageName *string, priorityClassName string) *corev1.Pod { func makeNodeImporterPodSpec(args *importerPodArgs) *corev1.Pod {
// importer pod name contains the pvc name // importer pod name contains the pvc name
podName, _ := pvc.Annotations[AnnImportPod] podName, _ := args.pvc.Annotations[AnnImportPod]
volumes := []corev1.Volume{
{
Name: "shared-volume",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
{
Name: DataVolName,
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: args.pvc.Name,
ReadOnly: false,
},
},
},
}
importerContainer := makeImporterContainerSpec(args.image, args.verbose, args.pullPolicy)
pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: args.pvc.Namespace,
Annotations: map[string]string{
AnnCreatedBy: "yes",
},
Labels: map[string]string{
common.CDILabelKey: common.CDILabelValue,
common.CDIComponentLabel: common.ImporterPodName,
common.PrometheusLabel: "",
},
},
Spec: corev1.PodSpec{
InitContainers: []corev1.Container{
{
Name: "init",
Image: args.image,
ImagePullPolicy: corev1.PullPolicy(args.pullPolicy),
Command: []string{"sh", "-c", "cp /usr/bin/cdi-containerimage-server /shared/server"},
VolumeMounts: []corev1.VolumeMount{
{
MountPath: "/shared",
Name: "shared-volume",
},
},
},
},
Containers: []corev1.Container{
*importerContainer,
{
Name: "server",
Image: args.importImage,
ImagePullPolicy: corev1.PullPolicy(args.pullPolicy),
Command: []string{"/shared/server", "-p", "8100", "-image-dir", "/disk", "-ready-file", "/shared/ready", "-done-file", "/shared/done"},
VolumeMounts: []corev1.VolumeMount{
{
MountPath: "/shared",
Name: "shared-volume",
},
},
},
},
RestartPolicy: corev1.RestartPolicyOnFailure,
Volumes: volumes,
NodeSelector: args.workloadNodePlacement.NodeSelector,
Tolerations: args.workloadNodePlacement.Tolerations,
Affinity: args.workloadNodePlacement.Affinity,
PriorityClassName: args.priorityClassName,
},
}
/**
FIXME: When registry source is ImageStream, if we set importer pod OwnerReference (to its pvc, like all other cases),
for some reason (OCP issue?) we get the following error:
Failed to pull image "imagestream-name": rpc error: code = Unknown
desc = Error reading manifest latest in docker.io/library/imagestream-name: errors:
denied: requested access to the resource is denied
unauthorized: authentication required
When we don't set pod OwnerReferences, all works well.
*/
if isImageStream(args.pvc) {
pod.Annotations[AnnOpenShiftImageLookup] = "*"
} else {
blockOwnerDeletion := true
isController := true
ownerRef := metav1.OwnerReference{
APIVersion: "v1",
Kind: "PersistentVolumeClaim",
Name: args.pvc.Name,
UID: args.pvc.GetUID(),
BlockOwnerDeletion: &blockOwnerDeletion,
Controller: &isController,
}
pod.OwnerReferences = append(pod.OwnerReferences, ownerRef)
}
args.podEnvVar.source = SourceHTTP
args.podEnvVar.ep = "http://localhost:8100/disk.img"
args.podEnvVar.readyFile = "/shared/ready"
args.podEnvVar.doneFile = "/shared/done"
setImporterPodCommons(pod, args.podEnvVar, args.pvc, args.podResourceRequirements)
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
MountPath: "/shared",
Name: "shared-volume",
})
return pod
}
// makeImporterPodSpec creates and return the importer pod spec based on the passed-in endpoint, secret and pvc.
func makeImporterPodSpec(args *importerPodArgs) *corev1.Pod {
// importer pod name contains the pvc name
podName, _ := args.pvc.Annotations[AnnImportPod]
blockOwnerDeletion := true blockOwnerDeletion := true
isController := true isController := true
@ -805,25 +1012,27 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar
Name: DataVolName, Name: DataVolName,
VolumeSource: corev1.VolumeSource{ VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: pvc.Name, ClaimName: args.pvc.Name,
ReadOnly: false, ReadOnly: false,
}, },
}, },
}, },
} }
if scratchPvcName != nil { if args.scratchPvcName != nil {
volumes = append(volumes, corev1.Volume{ volumes = append(volumes, corev1.Volume{
Name: ScratchVolName, Name: ScratchVolName,
VolumeSource: corev1.VolumeSource{ VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: *scratchPvcName, ClaimName: *args.scratchPvcName,
ReadOnly: false, ReadOnly: false,
}, },
}, },
}) })
} }
importerContainer := makeImporterContainerSpec(args.image, args.verbose, args.pullPolicy)
pod := &corev1.Pod{ pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "Pod", Kind: "Pod",
@ -831,7 +1040,7 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar
}, },
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: podName, Name: podName,
Namespace: namespace, Namespace: args.pvc.Namespace,
Annotations: map[string]string{ Annotations: map[string]string{
AnnCreatedBy: "yes", AnnCreatedBy: "yes",
}, },
@ -844,8 +1053,8 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar
{ {
APIVersion: "v1", APIVersion: "v1",
Kind: "PersistentVolumeClaim", Kind: "PersistentVolumeClaim",
Name: pvc.Name, Name: args.pvc.Name,
UID: pvc.GetUID(), UID: args.pvc.GetUID(),
BlockOwnerDeletion: &blockOwnerDeletion, BlockOwnerDeletion: &blockOwnerDeletion,
Controller: &isController, Controller: &isController,
}, },
@ -853,31 +1062,87 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar
}, },
Spec: corev1.PodSpec{ Spec: corev1.PodSpec{
Containers: []corev1.Container{ Containers: []corev1.Container{
{ *importerContainer,
Name: common.ImporterPodName,
Image: image,
ImagePullPolicy: corev1.PullPolicy(pullPolicy),
Args: []string{"-v=" + verbose},
Ports: []corev1.ContainerPort{
{
Name: "metrics",
ContainerPort: 8443,
Protocol: corev1.ProtocolTCP,
},
},
},
}, },
RestartPolicy: corev1.RestartPolicyOnFailure, RestartPolicy: corev1.RestartPolicyOnFailure,
Volumes: volumes, Volumes: volumes,
NodeSelector: workloadNodePlacement.NodeSelector, NodeSelector: args.workloadNodePlacement.NodeSelector,
Tolerations: workloadNodePlacement.Tolerations, Tolerations: args.workloadNodePlacement.Tolerations,
Affinity: workloadNodePlacement.Affinity, Affinity: args.workloadNodePlacement.Affinity,
PriorityClassName: priorityClassName, PriorityClassName: args.priorityClassName,
}, },
} }
setImporterPodCommons(pod, args.podEnvVar, args.pvc, args.podResourceRequirements)
if args.scratchPvcName != nil {
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
Name: ScratchVolName,
MountPath: common.ScratchDataDir,
})
}
if args.vddkImageName != nil {
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
Name: "vddk-vol-mount",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
})
pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
Name: "vddk-side-car",
Image: *args.vddkImageName,
VolumeMounts: []corev1.VolumeMount{
{
Name: "vddk-vol-mount",
MountPath: "/opt",
},
},
})
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
Name: "vddk-vol-mount",
MountPath: "/opt",
})
}
if args.podEnvVar.certConfigMap != "" {
vm := corev1.VolumeMount{
Name: CertVolName,
MountPath: common.ImporterCertDir,
}
vol := corev1.Volume{
Name: CertVolName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: args.podEnvVar.certConfigMap,
},
},
},
}
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, vm)
pod.Spec.Volumes = append(pod.Spec.Volumes, vol)
}
if args.podEnvVar.certConfigMapProxy != "" {
vm := corev1.VolumeMount{
Name: ProxyCertVolName,
MountPath: common.ImporterProxyCertDir,
}
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, vm)
pod.Spec.Volumes = append(pod.Spec.Volumes, createProxyConfigMapVolume(CertVolName, args.podEnvVar.certConfigMapProxy))
}
return pod
}
func setImporterPodCommons(pod *corev1.Pod, podEnvVar *importPodEnvVar, pvc *corev1.PersistentVolumeClaim, podResourceRequirements *corev1.ResourceRequirements) {
if podResourceRequirements != nil { if podResourceRequirements != nil {
pod.Spec.Containers[0].Resources = *podResourceRequirements for i := range pod.Spec.Containers {
pod.Spec.Containers[i].Resources = *podResourceRequirements
}
} }
ownerUID := pvc.UID ownerUID := pvc.UID
@ -894,68 +1159,8 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar
pod.Spec.Containers[0].VolumeMounts = addImportVolumeMounts() pod.Spec.Containers[0].VolumeMounts = addImportVolumeMounts()
} }
if scratchPvcName != nil {
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
Name: ScratchVolName,
MountPath: common.ScratchDataDir,
})
}
if vddkImageName != nil {
pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
Name: "vddk-vol-mount",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
})
pod.Spec.InitContainers = append(pod.Spec.InitContainers, corev1.Container{
Name: "vddk-side-car",
Image: *vddkImageName,
VolumeMounts: []corev1.VolumeMount{
{
Name: "vddk-vol-mount",
MountPath: "/opt",
},
},
})
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{
Name: "vddk-vol-mount",
MountPath: "/opt",
})
}
pod.Spec.Containers[0].Env = makeImportEnv(podEnvVar, ownerUID) pod.Spec.Containers[0].Env = makeImportEnv(podEnvVar, ownerUID)
if podEnvVar.certConfigMap != "" {
vm := corev1.VolumeMount{
Name: CertVolName,
MountPath: common.ImporterCertDir,
}
vol := corev1.Volume{
Name: CertVolName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: podEnvVar.certConfigMap,
},
},
},
}
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, vm)
pod.Spec.Volumes = append(pod.Spec.Volumes, vol)
}
if podEnvVar.certConfigMapProxy != "" {
vm := corev1.VolumeMount{
Name: ProxyCertVolName,
MountPath: common.ImporterProxyCertDir,
}
pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, vm)
pod.Spec.Volumes = append(pod.Spec.Volumes, createProxyConfigMapVolume(CertVolName, podEnvVar.certConfigMapProxy))
}
if podEnvVar.contentType == string(cdiv1.DataVolumeKubeVirt) { if podEnvVar.contentType == string(cdiv1.DataVolumeKubeVirt) {
// Set the fsGroup on the security context to the QemuSubGid // Set the fsGroup on the security context to the QemuSubGid
if pod.Spec.SecurityContext == nil { if pod.Spec.SecurityContext == nil {
@ -965,7 +1170,22 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar
pod.Spec.SecurityContext.FSGroup = &fsGroup pod.Spec.SecurityContext.FSGroup = &fsGroup
} }
SetPodPvcAnnotations(pod, pvc) SetPodPvcAnnotations(pod, pvc)
return pod }
func makeImporterContainerSpec(image, verbose, pullPolicy string) *corev1.Container {
return &corev1.Container{
Name: common.ImporterPodName,
Image: image,
ImagePullPolicy: corev1.PullPolicy(pullPolicy),
Args: []string{"-v=" + verbose},
Ports: []corev1.ContainerPort{
{
Name: "metrics",
ContainerPort: 8443,
Protocol: corev1.ProtocolTCP,
},
},
}
} }
func createProxyConfigMapVolume(certVolName, objRef string) corev1.Volume { func createProxyConfigMapVolume(certVolName, objRef string) corev1.Volume {
@ -1031,6 +1251,14 @@ func makeImportEnv(podEnvVar *importPodEnvVar, uid types.UID) []corev1.EnvVar {
Name: common.ImporterUUID, Name: common.ImporterUUID,
Value: podEnvVar.uuid, Value: podEnvVar.uuid,
}, },
{
Name: common.ImporterReadyFile,
Value: podEnvVar.readyFile,
},
{
Name: common.ImporterDoneFile,
Value: podEnvVar.doneFile,
},
{ {
Name: common.ImporterBackingFile, Name: common.ImporterBackingFile,
Value: podEnvVar.backingFile, Value: podEnvVar.backingFile,

View File

@ -693,7 +693,16 @@ var _ = Describe("Create Importer Pod", func() {
filesystemOverhead: "0.055", filesystemOverhead: "0.055",
insecureTLS: false, insecureTLS: false,
} }
pod, err := createImporterPod(reconciler.log, reconciler.client, testImage, "5", testPullPolicy, podEnvVar, pvc, scratchPvcName, nil, pvc.Annotations[AnnPriorityClassName], map[string]string{}) podArgs := &importerPodArgs{
image: testImage,
verbose: "5",
pullPolicy: testPullPolicy,
podEnvVar: podEnvVar,
pvc: pvc,
scratchPvcName: scratchPvcName,
priorityClassName: pvc.Annotations[AnnPriorityClassName],
}
pod, err := createImporterPod(reconciler.log, reconciler.client, podArgs, map[string]string{})
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
By("Verifying PVC owns pod") By("Verifying PVC owns pod")
Expect(len(pod.GetOwnerReferences())).To(Equal(1)) Expect(len(pod.GetOwnerReferences())).To(Equal(1))
@ -748,6 +757,8 @@ var _ = Describe("Import test env", func() {
certConfigMap: "", certConfigMap: "",
diskID: "", diskID: "",
uuid: "", uuid: "",
readyFile: "",
doneFile: "",
backingFile: "", backingFile: "",
thumbprint: "", thumbprint: "",
filesystemOverhead: "0.055", filesystemOverhead: "0.055",
@ -993,6 +1004,14 @@ func createImportTestEnv(podEnvVar *importPodEnvVar, uid string) []corev1.EnvVar
Name: common.ImporterUUID, Name: common.ImporterUUID,
Value: podEnvVar.uuid, Value: podEnvVar.uuid,
}, },
{
Name: common.ImporterReadyFile,
Value: podEnvVar.readyFile,
},
{
Name: common.ImporterDoneFile,
Value: podEnvVar.doneFile,
},
{ {
Name: common.ImporterBackingFile, Name: common.ImporterBackingFile,
Value: podEnvVar.backingFile, Value: podEnvVar.backingFile,

View File

@ -31,6 +31,7 @@ import (
"github.com/containers/image/v5/types" "github.com/containers/image/v5/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/klog/v2" "k8s.io/klog/v2"
cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/core/v1beta1"
"kubevirt.io/containerized-data-importer/pkg/util" "kubevirt.io/containerized-data-importer/pkg/util"
) )
@ -85,9 +86,9 @@ func parseImageName(img string) (types.ImageReference, error) {
return nil, errors.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, img) return nil, errors.Errorf(`Invalid image name "%s", expected colon-separated transport:reference`, img)
} }
switch parts[0] { switch parts[0] {
case "docker": case cdiv1.RegistrySchemeDocker:
return docker.ParseReference(parts[1]) return docker.ParseReference(parts[1])
case "oci-archive": case cdiv1.RegistrySchemeOci:
return archive.ParseReference(parts[1]) return archive.ParseReference(parts[1])
} }
return nil, errors.Errorf(`Invalid image name "%s", unknown transport`, img) return nil, errors.Errorf(`Invalid image name "%s", unknown transport`, img)

View File

@ -2296,14 +2296,18 @@ spec:
certConfigMap: certConfigMap:
description: CertConfigMap provides a reference to the Registry certs description: CertConfigMap provides a reference to the Registry certs
type: string type: string
imageStream:
description: ImageStream is the name of image stream for import
type: string
pullMethod:
description: PullMethod can be either "pod" (default import), or "node" (node docker cache based import)
type: string
secretRef: secretRef:
description: SecretRef provides the secret reference needed to access the Registry source description: SecretRef provides the secret reference needed to access the Registry source
type: string type: string
url: url:
description: URL is the url of the Docker registry source description: 'URL is the url of the registry source (starting with the scheme: docker, oci-archive)'
type: string type: string
required:
- url
type: object type: object
required: required:
- registry - registry
@ -3083,14 +3087,18 @@ spec:
certConfigMap: certConfigMap:
description: CertConfigMap provides a reference to the Registry certs description: CertConfigMap provides a reference to the Registry certs
type: string type: string
imageStream:
description: ImageStream is the name of image stream for import
type: string
pullMethod:
description: PullMethod can be either "pod" (default import), or "node" (node docker cache based import)
type: string
secretRef: secretRef:
description: SecretRef provides the secret reference needed to access the Registry source description: SecretRef provides the secret reference needed to access the Registry source
type: string type: string
url: url:
description: URL is the url of the Docker registry source description: 'URL is the url of the registry source (starting with the scheme: docker, oci-archive)'
type: string type: string
required:
- url
type: object type: object
s3: s3:
description: DataVolumeSourceS3 provides the parameters to create a Data Volume from an S3 source description: DataVolumeSourceS3 provides the parameters to create a Data Volume from an S3 source

View File

@ -1,4 +1,6 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
load("@io_bazel_rules_docker//container:container.bzl", "container_image")
load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar")
go_library( go_library(
name = "go_default_library", name = "go_default_library",
@ -116,3 +118,16 @@ exports_files(
"images/cirros-snapshot2.qcow2", "images/cirros-snapshot2.qcow2",
], ],
) )
pkg_tar(
name = "tinycore-tar",
srcs = [":images/tinyCore.iso"],
package_dir = "/disk",
strip_prefix = "/tests/images",
)
container_image(
name = "cdi-func-test-tinycore",
tars = [":tinycore-tar"],
visibility = ["//visibility:public"],
)

View File

@ -104,7 +104,7 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
dataVolume := utils.NewDataVolumeWithRegistryImport(dataVolumeName, size, url) dataVolume := utils.NewDataVolumeWithRegistryImport(dataVolumeName, size, url)
cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs) cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
dataVolume.Spec.Source.Registry.CertConfigMap = cm dataVolume.Spec.Source.Registry.CertConfigMap = &cm
return dataVolume return dataVolume
} }
@ -112,7 +112,7 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
dataVolume := utils.NewDataVolumeWithRegistryImport(dataVolumeName, size, url) dataVolume := utils.NewDataVolumeWithRegistryImport(dataVolumeName, size, url)
cm, err := utils.CopyFileHostCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs) cm, err := utils.CopyFileHostCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
dataVolume.Spec.Source.Registry.CertConfigMap = cm dataVolume.Spec.Source.Registry.CertConfigMap = &cm
return dataVolume return dataVolume
} }
@ -2151,13 +2151,13 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests",
}) })
Describe("Registry import with missing configmap", func() { Describe("Registry import with missing configmap", func() {
const cmName = "cert-registry-cm" cmName := "cert-registry-cm"
It("[test_id:4963]Import POD should remain pending until CM exists", func() { It("[test_id:4963]Import POD should remain pending until CM exists", func() {
var pvc *v1.PersistentVolumeClaim var pvc *v1.PersistentVolumeClaim
dataVolumeDef := utils.NewDataVolumeWithRegistryImport("missing-cm-registry-dv", "1Gi", tinyCoreIsoRegistryURL()) dataVolumeDef := utils.NewDataVolumeWithRegistryImport("missing-cm-registry-dv", "1Gi", tinyCoreIsoRegistryURL())
dataVolumeDef.Spec.Source.Registry.CertConfigMap = cmName dataVolumeDef.Spec.Source.Registry.CertConfigMap = &cmName
dataVolume, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolumeDef) dataVolume, err := utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dataVolumeDef)
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
f.ForceBindPvcIfDvIsWaitForFirstConsumer(dataVolume) f.ForceBindPvcIfDvIsWaitForFirstConsumer(dataVolume)

View File

@ -77,6 +77,8 @@ type Clients struct {
SnapshotSCName string SnapshotSCName string
BlockSCName string BlockSCName string
CsiCloneSCName string CsiCloneSCName string
DockerPrefix string
DockerTag string
// k8sClient provides our k8s client pointer // k8sClient provides our k8s client pointer
K8sClient *kubernetes.Clientset K8sClient *kubernetes.Clientset

View File

@ -184,7 +184,7 @@ var _ = Describe("Import Proxy tests", func() {
dv := utils.NewDataVolumeWithRegistryImport("import-dv", "1Gi", fmt.Sprintf(utils.TinyCoreIsoRegistryURL, f.CdiInstallNs)) dv := utils.NewDataVolumeWithRegistryImport("import-dv", "1Gi", fmt.Sprintf(utils.TinyCoreIsoRegistryURL, f.CdiInstallNs))
cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs) cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
dv.Spec.Source.Registry.CertConfigMap = cm dv.Spec.Source.Registry.CertConfigMap = &cm
dv, err = utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dv) dv, err = utils.CreateDataVolumeFromDefinition(f.CdiClient, f.Namespace.Name, dv)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
dvName = dv.Name dvName = dv.Name

View File

@ -1085,6 +1085,8 @@ var _ = Describe("Preallocation", func() {
md5PrefixSize = int64(100000) md5PrefixSize = int64(100000)
config *cdiv1.CDIConfig config *cdiv1.CDIConfig
origSpec *cdiv1.CDIConfigSpec origSpec *cdiv1.CDIConfigSpec
trustedRegistryURL = func() string { return fmt.Sprintf(utils.TrustedRegistryURL, f.DockerPrefix, f.DockerTag) }
trustedRegistryIS = func() string { return fmt.Sprintf(utils.TrustedRegistryIS, f.DockerPrefix, f.DockerTag) }
) )
BeforeEach(func() { BeforeEach(func() {
@ -1183,7 +1185,7 @@ var _ = Describe("Preallocation", func() {
Expect(ok).To(BeFalse()) Expect(ok).To(BeFalse())
}) })
DescribeTable("All import paths should contain Preallocation step", func(shouldPreallocate bool, expectedMD5, path string, dvFunc func() *cdiv1.DataVolume) { DescribeTable("[test_id:7241] All import paths should contain Preallocation step", func(shouldPreallocate bool, expectedMD5, path string, dvFunc func() *cdiv1.DataVolume) {
dv := dvFunc() dv := dvFunc()
By(fmt.Sprintf("Creating new datavolume %s", dv.Name)) By(fmt.Sprintf("Creating new datavolume %s", dv.Name))
preallocation := true preallocation := true
@ -1230,6 +1232,14 @@ var _ = Describe("Preallocation", func() {
} else { } else {
Expect(pvc.GetAnnotations()[controller.AnnPreallocationApplied]).ShouldNot(Equal("true")) Expect(pvc.GetAnnotations()[controller.AnnPreallocationApplied]).ShouldNot(Equal("true"))
} }
if dv.Spec.Source.Registry != nil && dv.Spec.Source.Registry.ImageStream != nil {
By("Verify image lookup annotation")
podName := pvc.Annotations[controller.AnnImportPod]
pod, err := f.K8sClient.CoreV1().Pods(f.Namespace.Name).Get(context.TODO(), podName, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred())
Expect(pod.Annotations[controller.AnnOpenShiftImageLookup]).To(Equal("*"))
}
}, },
Entry("HTTP import (ISO image)", true, utils.TinyCoreMD5, utils.DefaultImagePath, func() *cdiv1.DataVolume { Entry("HTTP import (ISO image)", true, utils.TinyCoreMD5, utils.DefaultImagePath, func() *cdiv1.DataVolume {
return utils.NewDataVolumeWithHTTPImport("import-dv", "100Mi", tinyCoreIsoURL()) return utils.NewDataVolumeWithHTTPImport("import-dv", "100Mi", tinyCoreIsoURL())
@ -1279,7 +1289,23 @@ var _ = Describe("Preallocation", func() {
dataVolume = utils.NewDataVolumeWithRegistryImport("import-dv", "100Mi", tinyCoreRegistryURL()) dataVolume = utils.NewDataVolumeWithRegistryImport("import-dv", "100Mi", tinyCoreRegistryURL())
cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs) cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
dataVolume.Spec.Source.Registry.CertConfigMap = cm dataVolume.Spec.Source.Registry.CertConfigMap = &cm
return dataVolume
}),
Entry("Registry node pull import", true, utils.TinyCoreMD5, utils.DefaultImagePath, func() *cdiv1.DataVolume {
pullMethod := cdiv1.RegistryPullNode
dataVolume = utils.NewDataVolumeWithRegistryImport("import-dv", "100Mi", trustedRegistryURL())
dataVolume.Spec.Source.Registry.PullMethod = &pullMethod
return dataVolume
}),
Entry("Registry ImageStream-wannabe node pull import", true, utils.TinyCoreMD5, utils.DefaultImagePath, func() *cdiv1.DataVolume {
pullMethod := cdiv1.RegistryPullNode
imageStreamWannabe := trustedRegistryIS()
dataVolume = utils.NewDataVolumeWithRegistryImport("import-dv", "100Mi", "")
dataVolume.Spec.Source.Registry.URL = nil
dataVolume.Spec.Source.Registry.ImageStream = &imageStreamWannabe
dataVolume.Spec.Source.Registry.PullMethod = &pullMethod
dataVolume.Annotations[controller.AnnPodRetainAfterCompletion] = "true"
return dataVolume return dataVolume
}), }),
Entry("VddkImport", true, utils.VcenterMD5, utils.DefaultImagePath, func() *cdiv1.DataVolume { Entry("VddkImport", true, utils.VcenterMD5, utils.DefaultImagePath, func() *cdiv1.DataVolume {

View File

@ -32,6 +32,8 @@ var (
snapshotSCName = flag.String("snapshot-sc", "", "The Storage Class supporting snapshots") snapshotSCName = flag.String("snapshot-sc", "", "The Storage Class supporting snapshots")
blockSCName = flag.String("block-sc", "", "The Storage Class supporting block mode volumes") blockSCName = flag.String("block-sc", "", "The Storage Class supporting block mode volumes")
csiCloneSCName = flag.String("csiclone-sc", "", "The Storage Class supporting CSI Volume Cloning") csiCloneSCName = flag.String("csiclone-sc", "", "The Storage Class supporting CSI Volume Cloning")
dockerPrefix = flag.String("docker-prefix", "", "The docker host:port")
dockerTag = flag.String("docker-tag", "", "The docker tag")
) )
func TestTests(t *testing.T) { func TestTests(t *testing.T) {
@ -57,6 +59,8 @@ func BuildTestSuite() {
framework.ClientsInstance.SnapshotSCName = *snapshotSCName framework.ClientsInstance.SnapshotSCName = *snapshotSCName
framework.ClientsInstance.BlockSCName = *blockSCName framework.ClientsInstance.BlockSCName = *blockSCName
framework.ClientsInstance.CsiCloneSCName = *csiCloneSCName framework.ClientsInstance.CsiCloneSCName = *csiCloneSCName
framework.ClientsInstance.DockerPrefix = *dockerPrefix
framework.ClientsInstance.DockerTag = *dockerTag
fmt.Fprintf(ginkgo.GinkgoWriter, "Kubectl path: %s\n", framework.ClientsInstance.KubectlPath) fmt.Fprintf(ginkgo.GinkgoWriter, "Kubectl path: %s\n", framework.ClientsInstance.KubectlPath)
fmt.Fprintf(ginkgo.GinkgoWriter, "OC path: %s\n", framework.ClientsInstance.OcPath) fmt.Fprintf(ginkgo.GinkgoWriter, "OC path: %s\n", framework.ClientsInstance.OcPath)
@ -67,6 +71,8 @@ func BuildTestSuite() {
fmt.Fprintf(ginkgo.GinkgoWriter, "Snapshot SC: %s\n", framework.ClientsInstance.SnapshotSCName) fmt.Fprintf(ginkgo.GinkgoWriter, "Snapshot SC: %s\n", framework.ClientsInstance.SnapshotSCName)
fmt.Fprintf(ginkgo.GinkgoWriter, "Block SC: %s\n", framework.ClientsInstance.BlockSCName) fmt.Fprintf(ginkgo.GinkgoWriter, "Block SC: %s\n", framework.ClientsInstance.BlockSCName)
fmt.Fprintf(ginkgo.GinkgoWriter, "CSI Volume Cloning SC: %s\n", framework.ClientsInstance.CsiCloneSCName) fmt.Fprintf(ginkgo.GinkgoWriter, "CSI Volume Cloning SC: %s\n", framework.ClientsInstance.CsiCloneSCName)
fmt.Fprintf(ginkgo.GinkgoWriter, "DockerPrefix: %s\n", framework.ClientsInstance.DockerPrefix)
fmt.Fprintf(ginkgo.GinkgoWriter, "DockerTag: %s\n", framework.ClientsInstance.DockerTag)
restConfig, err := framework.ClientsInstance.LoadConfig() restConfig, err := framework.ClientsInstance.LoadConfig()
if err != nil { if err != nil {

View File

@ -2,12 +2,14 @@ package tests
import ( import (
"fmt" "fmt"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/core/v1beta1"
"kubevirt.io/containerized-data-importer/pkg/common" "kubevirt.io/containerized-data-importer/pkg/common"
"kubevirt.io/containerized-data-importer/pkg/controller" "kubevirt.io/containerized-data-importer/pkg/controller"
"kubevirt.io/containerized-data-importer/tests/framework" "kubevirt.io/containerized-data-importer/tests/framework"
@ -24,6 +26,7 @@ var _ = Describe("Transport Tests", func() {
targetRawImage = "tinycoreqcow2" targetRawImage = "tinycoreqcow2"
targetArchivedImage = "tinycoreisotar" targetArchivedImage = "tinycoreisotar"
targetArchivedImageHash = "b354a50183e70ee2ed3413eea67fe153" targetArchivedImageHash = "b354a50183e70ee2ed3413eea67fe153"
targetNodePullImage = "cdi-func-test-tinycore"
) )
var ( var (
@ -40,16 +43,24 @@ var _ = Describe("Transport Tests", func() {
// it() is the body of the test and is executed once per Entry() by DescribeTable() // it() is the body of the test and is executed once per Entry() by DescribeTable()
// closes over c and ns // closes over c and ns
it := func(ep func() string, file, expectedHash, accessKey, secretKey, source, certConfigMap string, insecureRegistry, shouldSucceed bool) { it := func(ep func() string, file, expectedHash, accessKey, secretKey, source, certConfigMap, registryImportMethod string, insecureRegistry, shouldSucceed bool) {
var ( var (
err error // prevent shadowing err error // prevent shadowing
) )
var endpoint string
if registryImportMethod == string(cdiv1.RegistryPullNode) {
endpoint = ep() + "/" + file + ":" + f.DockerTag
} else {
endpoint = ep() + "/" + file
}
pvcAnn := map[string]string{ pvcAnn := map[string]string{
controller.AnnEndpoint: ep() + "/" + file, controller.AnnEndpoint: endpoint,
controller.AnnSecret: "", controller.AnnSecret: "",
controller.AnnSource: source, controller.AnnSource: source,
controller.AnnRegistryImportMethod: registryImportMethod,
} }
if accessKey != "" || secretKey != "" { if accessKey != "" || secretKey != "" {
@ -127,7 +138,6 @@ var _ = Describe("Transport Tests", func() {
}, timeout, pollingInterval).Should(BeTrue()) }, timeout, pollingInterval).Should(BeTrue())
} }
} }
httpNoAuthEp := func() string { httpNoAuthEp := func() string {
return fmt.Sprintf("http://%s:%d", utils.FileHostName+"."+f.CdiInstallNs, utils.HTTPNoAuthPort) return fmt.Sprintf("http://%s:%d", utils.FileHostName+"."+f.CdiInstallNs, utils.HTTPNoAuthPort)
} }
@ -140,23 +150,26 @@ var _ = Describe("Transport Tests", func() {
registryNoAuthEp := func() string { return fmt.Sprintf("docker://%s", utils.RegistryHostName+"."+f.CdiInstallNs) } registryNoAuthEp := func() string { return fmt.Sprintf("docker://%s", utils.RegistryHostName+"."+f.CdiInstallNs) }
registryAuthEp := func() string { return fmt.Sprintf("docker://%s.%s:%d", utils.RegistryHostName, f.CdiInstallNs, 1443) } registryAuthEp := func() string { return fmt.Sprintf("docker://%s.%s:%d", utils.RegistryHostName, f.CdiInstallNs, 1443) }
altRegistryNoAuthEp := func() string { return fmt.Sprintf("docker://%s.%s:%d", utils.RegistryHostName, f.CdiInstallNs, 5000) } altRegistryNoAuthEp := func() string { return fmt.Sprintf("docker://%s.%s:%d", utils.RegistryHostName, f.CdiInstallNs, 5000) }
trustedRegistryEp := func() string { return fmt.Sprintf("docker://%s", f.DockerPrefix) }
DescribeTable("Transport Test Table", it, DescribeTable("Transport Test Table", it,
Entry("[test_id:5059]should connect to http endpoint without credentials", httpNoAuthEp, targetFile, "", "", "", controller.SourceHTTP, "", false, true), Entry("[test_id:5059]should connect to http endpoint without credentials", httpNoAuthEp, targetFile, "", "", "", controller.SourceHTTP, "", "", false, true),
Entry("[test_id:5060]should connect to http endpoint with credentials", httpAuthEp, targetFile, "", utils.AccessKeyValue, utils.SecretKeyValue, controller.SourceHTTP, "", false, true), Entry("[test_id:5060]should connect to http endpoint with credentials", httpAuthEp, targetFile, "", utils.AccessKeyValue, utils.SecretKeyValue, controller.SourceHTTP, "", "", false, true),
Entry("[test_id:5061]should not connect to http endpoint with invalid credentials", httpAuthEp, targetFile, "", "invalid", "invalid", controller.SourceHTTP, "", false, false), Entry("[test_id:5061]should not connect to http endpoint with invalid credentials", httpAuthEp, targetFile, "", "invalid", "invalid", controller.SourceHTTP, "", "", false, false),
Entry("[test_id:5062]should connect to QCOW http endpoint without credentials", httpNoAuthEp, targetQCOWFile, utils.UploadFileMD5, "", "", controller.SourceHTTP, "", false, true), Entry("[test_id:5062]should connect to QCOW http endpoint without credentials", httpNoAuthEp, targetQCOWFile, utils.UploadFileMD5, "", "", controller.SourceHTTP, "", "", false, true),
Entry("[test_id:5063]should connect to QCOW http endpoint with credentials", httpAuthEp, targetQCOWFile, utils.UploadFileMD5, utils.AccessKeyValue, utils.SecretKeyValue, controller.SourceHTTP, "", false, true), Entry("[test_id:5063]should connect to QCOW http endpoint with credentials", httpAuthEp, targetQCOWFile, utils.UploadFileMD5, utils.AccessKeyValue, utils.SecretKeyValue, controller.SourceHTTP, "", "", false, true),
Entry("[test_id:5064]should succeed to import from registry when image contains valid qcow file, custom cert", registryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "cdi-docker-registry-host-certs", false, true), Entry("[test_id:5064]should succeed to import from registry when image contains valid qcow file, custom cert", registryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "cdi-docker-registry-host-certs", "", false, true),
Entry("[test_id:5065]should fail to import from registry when image contains valid qcow file, custom cert+auth, invalid credentials", registryAuthEp, targetQCOWImage, utils.UploadFileMD5, "invalid", "invalid", controller.SourceRegistry, "cdi-docker-registry-host-certs", true, false), Entry("[test_id:5065]should fail to import from registry when image contains valid qcow file, custom cert+auth, invalid credentials", registryAuthEp, targetQCOWImage, utils.UploadFileMD5, "invalid", "invalid", controller.SourceRegistry, "cdi-docker-registry-host-certs", "", true, false),
Entry("[test_id:5066]should succeed to import from registry when image contains valid qcow file, custom cert+auth, valid credentials", registryAuthEp, targetQCOWImage, utils.UploadFileMD5, utils.AccessKeyValue, utils.SecretKeyValue, controller.SourceRegistry, "cdi-docker-registry-host-certs", true, true), Entry("[test_id:5066]should succeed to import from registry when image contains valid qcow file, custom cert+auth, valid credentials", registryAuthEp, targetQCOWImage, utils.UploadFileMD5, utils.AccessKeyValue, utils.SecretKeyValue, controller.SourceRegistry, "cdi-docker-registry-host-certs", "", true, true),
Entry("[test_id:5067]should succeed to import from registry when image contains valid qcow file, no auth", registryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "", true, true), Entry("[test_id:5067]should succeed to import from registry when image contains valid qcow file, no auth", registryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "", "", true, true),
Entry("[test_id:5068]should succeed to import from registry when image contains valid qcow file, auth", altRegistryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "", true, true), Entry("[test_id:5068]should succeed to import from registry when image contains valid qcow file, auth", altRegistryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "", "", true, true),
Entry("[test_id:5069]should fail no certs", registryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "", false, false), Entry("[test_id:5069]should fail no certs", registryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "", "", false, false),
Entry("[test_id:5070]should fail bad certs", registryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "cdi-file-host-certs", false, false), Entry("[test_id:5070]should fail bad certs", registryNoAuthEp, targetQCOWImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "cdi-file-host-certs", "", false, false),
Entry("[test_id:5071]should succeed to import from registry when image contains valid raw file", registryNoAuthEp, targetRawImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "cdi-docker-registry-host-certs", false, true), Entry("[test_id:5071]should succeed to import from registry when image contains valid raw file", registryNoAuthEp, targetRawImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "cdi-docker-registry-host-certs", "", false, true),
Entry("[test_id:5072]should succeed to import from registry when image contains valid archived raw file", registryNoAuthEp, targetArchivedImage, targetArchivedImageHash, "", "", controller.SourceRegistry, "cdi-docker-registry-host-certs", false, true), Entry("[test_id:5072]should succeed to import from registry when image contains valid archived raw file", registryNoAuthEp, targetArchivedImage, targetArchivedImageHash, "", "", controller.SourceRegistry, "cdi-docker-registry-host-certs", "", false, true),
Entry("[test_id:5073]should not connect to https endpoint without cert", httpsNoAuthEp, targetFile, "", "", "", controller.SourceHTTP, "", false, false), Entry("[test_id:5073]should not connect to https endpoint without cert", httpsNoAuthEp, targetFile, "", "", "", controller.SourceHTTP, "", "", false, false),
Entry("[test_id:5074]should connect to https endpoint with cert", httpsNoAuthEp, targetFile, "", "", "", controller.SourceHTTP, "cdi-file-host-certs", false, true), Entry("[test_id:5074]should connect to https endpoint with cert", httpsNoAuthEp, targetFile, "", "", "", controller.SourceHTTP, "cdi-file-host-certs", "", false, true),
Entry("[test_id:5075]should not connect to https endpoint with bad cert", httpsNoAuthEp, targetFile, "", "", "", controller.SourceHTTP, "cdi-docker-registry-host-certs", false, false), Entry("[test_id:5075]should not connect to https endpoint with bad cert", httpsNoAuthEp, targetFile, "", "", "", controller.SourceHTTP, "cdi-docker-registry-host-certs", "", false, false),
Entry("[test_id:7240]should succeed to node pull import from registry when image contains valid iso file, no auth", trustedRegistryEp, targetNodePullImage, utils.UploadFileMD5, "", "", controller.SourceRegistry, "", string(cdiv1.RegistryPullNode), false, true),
) )
}) })

View File

@ -73,6 +73,10 @@ const (
ImageioImageURL = "https://imageio.%s:12345" ImageioImageURL = "https://imageio.%s:12345"
// VcenterURL provides URL of vCenter/ESX simulator // VcenterURL provides URL of vCenter/ESX simulator
VcenterURL = "https://vcenter.%s:8989/sdk" VcenterURL = "https://vcenter.%s:8989/sdk"
// TrustedRegistryURL provides the base path to trusted registry test url for the tinycore.iso image wrapped in docker container
TrustedRegistryURL = "docker://%s/cdi-func-test-tinycore:%s"
// TrustedRegistryIS provides the base path to trusted registry test fake imagestream for the tinycore.iso image wrapped in docker container
TrustedRegistryIS = "%s/cdi-func-test-tinycore:%s"
// TinyCoreMD5 is the MD5 hash of first 100k bytes of tinyCore image // TinyCoreMD5 is the MD5 hash of first 100k bytes of tinyCore image
TinyCoreMD5 = "3710416a680523c7d07538cb1026c60c" TinyCoreMD5 = "3710416a680523c7d07538cb1026c60c"
@ -440,12 +444,13 @@ func NewDataVolumeForImageCloning(dataVolumeName, size, namespace, pvcName strin
func NewDataVolumeWithRegistryImport(dataVolumeName string, size string, registryURL string) *cdiv1.DataVolume { func NewDataVolumeWithRegistryImport(dataVolumeName string, size string, registryURL string) *cdiv1.DataVolume {
return &cdiv1.DataVolume{ return &cdiv1.DataVolume{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: dataVolumeName, Name: dataVolumeName,
Annotations: map[string]string{},
}, },
Spec: cdiv1.DataVolumeSpec{ Spec: cdiv1.DataVolumeSpec{
Source: &cdiv1.DataVolumeSource{ Source: &cdiv1.DataVolumeSource{
Registry: &cdiv1.DataVolumeSourceRegistry{ Registry: &cdiv1.DataVolumeSourceRegistry{
URL: registryURL, URL: &registryURL,
}, },
}, },
PVC: &k8sv1.PersistentVolumeClaimSpec{ PVC: &k8sv1.PersistentVolumeClaimSpec{

View File

@ -0,0 +1,16 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "kubevirt.io/containerized-data-importer/tools/cdi-containerimage-server",
visibility = ["//visibility:private"],
deps = ["//vendor/github.com/pkg/errors:go_default_library"],
)
go_binary(
name = "cdi-containerimage-server",
embed = [":go_default_library"],
pure = "on",
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,93 @@
package main
import (
"context"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
)
func printFiles(dir string) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
fmt.Println(path)
return nil
})
}
func renameImageFile(dir, newName string) error {
entries, err := ioutil.ReadDir(dir)
if err != nil {
return err
}
if len(entries) != 1 || entries[0].IsDir() {
return errors.Errorf("Invalid container image")
}
src := filepath.Join(dir, entries[0].Name())
target := filepath.Join(dir, newName)
if err := os.Rename(src, target); err != nil {
return err
}
return nil
}
func main() {
port := flag.Int("p", 8100, "server port")
directory := flag.String("image-dir", ".", "directory to serve")
readyFile := flag.String("ready-file", "/shared/ready", "file to create when ready for connections")
doneFile := flag.String("done-file", "/shared/done", "file created when the client is done")
imageName := flag.String("image-name", "disk.img", "name of the image to serve up")
flag.Parse()
if err := printFiles(*directory); err != nil {
log.Fatalf("Failed walking the directory %s: %v", *directory, err)
}
if err := renameImageFile(*directory, *imageName); err != nil {
log.Fatalf("Failed renaming image file %s, directory %s: %v", *imageName, *directory, err)
}
server := &http.Server{
Handler: http.FileServer(http.Dir(*directory)),
}
addr := fmt.Sprintf("localhost:%d", *port)
listener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Failed listening on %s err: %v", addr, err)
}
f, err := os.OpenFile(*readyFile, os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
log.Fatalf("Failed creating \"ready\" file: %v", err)
}
defer os.Remove(*readyFile)
f.Close()
go func() {
log.Printf("Serving %s on HTTP port: %d\n", *directory, *port)
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
log.Fatalf("Serve failed: %v", err)
}
}()
for {
if _, err := os.Stat(*doneFile); err == nil {
break
}
time.Sleep(time.Second)
}
os.Remove(*doneFile)
if err := server.Shutdown(context.TODO()); err != nil {
log.Printf("Shutdown failed: %v\n", err)
}
log.Println("Importer has completed")
}