From addf25b4f9ee2d7ad83a9be68bb17c5765c2570c Mon Sep 17 00:00:00 2001 From: Arnon Gilboa Date: Mon, 20 Sep 2021 23:05:36 +0300 Subject: [PATCH] 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 * Add tests, fix CR comments Signed-off-by: Arnon Gilboa * CR fixes Signed-off-by: Arnon Gilboa * Use deployment docker prefix and tag in func tests Signed-off-by: Arnon Gilboa * Add OpenShift ImageStreams import support Signed-off-by: Arnon Gilboa * Add importer pod lookup annotation for image streams Signed-off-by: Arnon Gilboa * Add pullMethod and imageStream doc Signed-off-by: Arnon Gilboa --- BUILD.bazel | 2 + api/openapi-spec/swagger.json | 16 +- cmd/cdi-importer/BUILD.bazel | 1 + cmd/cdi-importer/importer.go | 28 ++ doc/image-from-registry.md | 44 ++ hack/build/run-functional-tests.sh | 8 +- pkg/apis/core/v1beta1/openapi_generated.go | 18 +- pkg/apis/core/v1beta1/types.go | 34 +- .../core/v1beta1/types_swagger_generated.go | 8 +- .../core/v1beta1/zz_generated.deepcopy.go | 29 +- pkg/apiserver/webhooks/datavolume-validate.go | 79 +++- .../webhooks/datavolume-validate_test.go | 82 +++- pkg/common/common.go | 4 + pkg/controller/datavolume-controller.go | 27 +- pkg/controller/import-controller.go | 436 +++++++++++++----- pkg/controller/import-controller_test.go | 21 +- pkg/importer/transport.go | 5 +- pkg/operator/resources/crds_generated.go | 20 +- tests/BUILD.bazel | 15 + tests/datavolume_test.go | 8 +- tests/framework/framework.go | 2 + tests/import_proxy_test.go | 2 +- tests/import_test.go | 30 +- tests/tests_suite_test.go | 6 + tests/transport_test.go | 57 ++- tests/utils/datavolume.go | 9 +- tools/cdi-containerimage-server/BUILD.bazel | 16 + tools/cdi-containerimage-server/main.go | 93 ++++ 28 files changed, 919 insertions(+), 181 deletions(-) create mode 100644 tools/cdi-containerimage-server/BUILD.bazel create mode 100755 tools/cdi-containerimage-server/main.go diff --git a/BUILD.bazel b/BUILD.bazel index 87a41b9c6..62520fb44 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -58,6 +58,7 @@ container_bundle( "$(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-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)/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)/cdi-func-test-tinycore:$(container_tag)": "//tests:cdi-func-test-tinycore", }, ) diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 3e34d4f4d..e2127273f 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -3935,22 +3935,26 @@ "v1beta1.DataVolumeSourceRegistry": { "description": "DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source", "type": "object", - "required": [ - "url" - ], "properties": { "certConfigMap": { "description": "CertConfigMap provides a reference to the Registry certs", "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": { "description": "SecretRef provides the secret reference needed to access the Registry source", "type": "string" }, "url": { - "description": "URL is the url of the Docker registry source", - "type": "string", - "default": "" + "description": "URL is the url of the registry source (starting with the scheme: docker, oci-archive)", + "type": "string" } } }, diff --git a/cmd/cdi-importer/BUILD.bazel b/cmd/cdi-importer/BUILD.bazel index 613abd7cc..e52e56459 100644 --- a/cmd/cdi-importer/BUILD.bazel +++ b/cmd/cdi-importer/BUILD.bazel @@ -89,6 +89,7 @@ container_image( ], files = [ ":cdi-importer", + "//tools/cdi-containerimage-server", ], visibility = ["//visibility:public"], ) diff --git a/cmd/cdi-importer/importer.go b/cmd/cdi-importer/importer.go index 1bfd9ba90..ebc8b289e 100644 --- a/cmd/cdi-importer/importer.go +++ b/cmd/cdi-importer/importer.go @@ -18,6 +18,7 @@ import ( "io/ioutil" "os" "strconv" + "time" "github.com/pkg/errors" @@ -39,6 +40,31 @@ func init() { 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() { defer klog.Flush() @@ -186,6 +212,7 @@ func main() { } defer dp.Close() processor := importer.NewDataProcessor(dp, dest, dataDir, common.ScratchDataDir, imageSize, filesystemOverhead, preallocation) + waitForReadyFile() err = processor.ProcessData() if err != nil { klog.Errorf("%+v", err) @@ -200,6 +227,7 @@ func main() { dp.Close() os.Exit(1) } + touchDoneFile() preallocationApplied = processor.PreallocationApplied() } message := "Import Complete" diff --git a/doc/image-from-registry.md b/doc/image-from-registry.md index aa75620b4..7e4426f19 100644 --- a/doc/image-from-registry.md +++ b/doc/image-from-registry.md @@ -126,3 +126,47 @@ Add the registry to CDIConfig insecureRegistries in the `cdi` namespace. ```bash 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). \ No newline at end of file diff --git a/hack/build/run-functional-tests.sh b/hack/build/run-functional-tests.sh index 60b5486cc..8583f43f8 100755 --- a/hack/build/run-functional-tests.sh +++ b/hack/build/run-functional-tests.sh @@ -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 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 kubevirtci_kubectl="${BASE_PATH}/${KUBEVIRT_PROVIDER}/.kubectl" 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_block="${BLOCK_SC:+-block-sc=$BLOCK_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' retry_counter=0 diff --git a/pkg/apis/core/v1beta1/openapi_generated.go b/pkg/apis/core/v1beta1/openapi_generated.go index 129a9c728..0db9c9ed1 100644 --- a/pkg/apis/core/v1beta1/openapi_generated.go +++ b/pkg/apis/core/v1beta1/openapi_generated.go @@ -15714,8 +15714,21 @@ func schema_pkg_apis_core_v1beta1_DataVolumeSourceRegistry(ref common.ReferenceC Properties: map[string]spec.Schema{ "url": { SchemaProps: spec.SchemaProps{ - Description: "URL is the url of the Docker registry source", - Default: "", + Description: "URL is the url of the registry source (starting with the scheme: docker, oci-archive)", + 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"}, Format: "", }, @@ -15735,7 +15748,6 @@ func schema_pkg_apis_core_v1beta1_DataVolumeSourceRegistry(ref common.ReferenceC }, }, }, - Required: []string{"url"}, }, }, } diff --git a/pkg/apis/core/v1beta1/types.go b/pkg/apis/core/v1beta1/types.go index d7c76207d..801c55cd7 100644 --- a/pkg/apis/core/v1beta1/types.go +++ b/pkg/apis/core/v1beta1/types.go @@ -152,14 +152,40 @@ type DataVolumeSourceS3 struct { // DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source type DataVolumeSourceRegistry struct { - //URL is the url of the Docker registry source - URL string `json:"url"` + //URL is the url of the registry source (starting with the scheme: docker, oci-archive) + // +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 string `json:"secretRef,omitempty"` + // +optional + SecretRef *string `json:"secretRef,omitempty"` //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 type DataVolumeSourceHTTP struct { // URL is the URL of the http(s) endpoint diff --git a/pkg/apis/core/v1beta1/types_swagger_generated.go b/pkg/apis/core/v1beta1/types_swagger_generated.go index 39309f380..559a5597c 100644 --- a/pkg/apis/core/v1beta1/types_swagger_generated.go +++ b/pkg/apis/core/v1beta1/types_swagger_generated.go @@ -82,9 +82,11 @@ func (DataVolumeSourceS3) SwaggerDoc() map[string]string { func (DataVolumeSourceRegistry) SwaggerDoc() map[string]string { return map[string]string{ "": "DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source", - "url": "URL is the url of the Docker registry source", - "secretRef": "SecretRef provides the secret reference needed to access the Registry source", - "certConfigMap": "CertConfigMap provides a reference to the Registry certs", + "url": "URL is the url of the registry source (starting with the scheme: docker, oci-archive)\n+optional", + "imageStream": "ImageStream is the name of image stream for import\n+optional", + "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", } } diff --git a/pkg/apis/core/v1beta1/zz_generated.deepcopy.go b/pkg/apis/core/v1beta1/zz_generated.deepcopy.go index a1a73731a..91c9e2218 100644 --- a/pkg/apis/core/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/core/v1beta1/zz_generated.deepcopy.go @@ -468,7 +468,7 @@ func (in *DataImportCronSource) DeepCopyInto(out *DataImportCronSource) { if in.Registry != nil { in, out := &in.Registry, &out.Registry *out = new(DataVolumeSourceRegistry) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -808,7 +808,7 @@ func (in *DataVolumeSource) DeepCopyInto(out *DataVolumeSource) { if in.Registry != nil { in, out := &in.Registry, &out.Registry *out = new(DataVolumeSourceRegistry) - **out = **in + (*in).DeepCopyInto(*out) } if in.PVC != nil { 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. func (in *DataVolumeSourceRegistry) DeepCopyInto(out *DataVolumeSourceRegistry) { *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 } diff --git a/pkg/apiserver/webhooks/datavolume-validate.go b/pkg/apiserver/webhooks/datavolume-validate.go index 17c9970d6..fc8b331f5 100644 --- a/pkg/apiserver/webhooks/datavolume-validate.go +++ b/pkg/apiserver/webhooks/datavolume-validate.go @@ -23,7 +23,7 @@ import ( "context" "encoding/json" "fmt" - "net/url" + neturl "net/url" "reflect" admissionv1 "k8s.io/api/admission/v1" @@ -50,7 +50,7 @@ func validateSourceURL(sourceURL string) string { if sourceURL == "" { return "source URL is empty" } - url, err := url.ParseRequestURI(sourceURL) + url, err := neturl.ParseRequestURI(sourceURL) if err != nil { return fmt.Sprintf("Invalid source URL: %s", sourceURL) } @@ -240,14 +240,73 @@ func (wh *dataVolumeValidatingWebhook) validateDataVolumeSpec(request *admission return causes } - if spec.Source.Registry != nil && spec.ContentType != "" && string(spec.ContentType) != string(cdiv1.DataVolumeKubeVirt) { - sourceType = field.Child("contentType").String() - causes = append(causes, metav1.StatusCause{ - Type: metav1.CauseTypeFieldValueInvalid, - Message: fmt.Sprintf("ContentType must be " + string(cdiv1.DataVolumeKubeVirt) + " when Source is Registry"), - Field: sourceType, - }) - return causes + if spec.Source.Registry != nil { + if spec.ContentType != "" && string(spec.ContentType) != string(cdiv1.DataVolumeKubeVirt) { + sourceType = field.Child("contentType").String() + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Message: fmt.Sprintf("ContentType must be %s when Source is Registry", cdiv1.DataVolumeKubeVirt), + Field: sourceType, + }) + 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 { diff --git a/pkg/apiserver/webhooks/datavolume-validate_test.go b/pkg/apiserver/webhooks/datavolume-validate_test.go index 5483a24cc..be068c9b1 100644 --- a/pkg/apiserver/webhooks/datavolume-validate_test.go +++ b/pkg/apiserver/webhooks/datavolume-validate_test.go @@ -68,12 +68,90 @@ var _ = Describe("Validating Webhook", func() { 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") resp := validateDataVolumeCreate(dataVolume) 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() { dataVolume := newPVCDataVolume("testDV", "testNamespace", "test") pvc := &corev1.PersistentVolumeClaim{ @@ -520,7 +598,7 @@ func newHTTPDataVolume(name, url string) *cdiv1.DataVolume { func newRegistryDataVolume(name, url string) *cdiv1.DataVolume { registrySource := cdiv1.DataVolumeSource{ - Registry: &cdiv1.DataVolumeSourceRegistry{URL: url}, + Registry: &cdiv1.DataVolumeSourceRegistry{URL: &url}, } pvc := newPVCSpec(pvcSizeDefault) return newDataVolume(name, registrySource, pvc) diff --git a/pkg/common/common.go b/pkg/common/common.go index 4aea908cc..28298492e 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -88,6 +88,10 @@ const ( ImporterDiskID = "IMPORTER_DISK_ID" // ImporterUUID provides a constant to capture our env variable "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 = "IMPORTER_BACKING_FILE" // ImporterThumbprint provides a constant to capture our env variable "IMPORTER_THUMBPRINT" diff --git a/pkg/controller/datavolume-controller.go b/pkg/controller/datavolume-controller.go index 722742bcf..ef72fa722 100644 --- a/pkg/controller/datavolume-controller.go +++ b/pkg/controller/datavolume-controller.go @@ -2287,13 +2287,28 @@ func (r *DatavolumeReconciler) newPersistentVolumeClaim(dataVolume *cdiv1.DataVo } } else if dataVolume.Spec.Source.Registry != nil { annotations[AnnSource] = SourceRegistry - annotations[AnnEndpoint] = dataVolume.Spec.Source.Registry.URL - annotations[AnnContentType] = string(dataVolume.Spec.ContentType) - if dataVolume.Spec.Source.Registry.SecretRef != "" { - annotations[AnnSecret] = dataVolume.Spec.Source.Registry.SecretRef + pullMethod := dataVolume.Spec.Source.Registry.PullMethod + if pullMethod != nil && *pullMethod != "" { + annotations[AnnRegistryImportMethod] = string(*pullMethod) } - if dataVolume.Spec.Source.Registry.CertConfigMap != "" { - annotations[AnnCertConfigMap] = dataVolume.Spec.Source.Registry.CertConfigMap + url := dataVolume.Spec.Source.Registry.URL + 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 { sourceNamespace := dataVolume.Spec.Source.PVC.Namespace diff --git a/pkg/controller/import-controller.go b/pkg/controller/import-controller.go index 693eaace6..24244ceb4 100644 --- a/pkg/controller/import-controller.go +++ b/pkg/controller/import-controller.go @@ -8,8 +8,6 @@ import ( "strconv" "time" - sdkapi "kubevirt.io/controller-lifecycle-operator-sdk/pkg/sdk/api" - "github.com/go-logr/logr" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -31,6 +29,7 @@ import ( featuregates "kubevirt.io/containerized-data-importer/pkg/feature-gates" "kubevirt.io/containerized-data-importer/pkg/util" "kubevirt.io/containerized-data-importer/pkg/util/naming" + sdkapi "kubevirt.io/controller-lifecycle-operator-sdk/pkg/sdk/api" ) const ( @@ -61,6 +60,10 @@ const ( AnnCertConfigMap = AnnAPIGroup + "/storage.import.certConfigMap" // AnnContentType provides a const for the PVC content-type 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 = AnnAPIGroup + "/storage.import.importPodName" // 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 = "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 = "ErrImportFailed" // 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 = "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 @@ -117,6 +127,8 @@ type importPodEnvVar struct { certConfigMap string diskID string uuid string + readyFile string + doneFile string backingFile string thumbprint string filesystemOverhead string @@ -131,6 +143,20 @@ type importPodEnvVar struct { 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. 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{ @@ -196,6 +222,10 @@ func isPVCComplete(pvc *corev1.PersistentVolumeClaim) bool { 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. func (r *ImportReconciler) Reconcile(_ context.Context, req reconcile.Request) (reconcile.Result, error) { log := r.log.WithValues("PVC", req.NamespacedName) @@ -249,11 +279,9 @@ func (r *ImportReconciler) findImporterPod(pvc *corev1.PersistentVolumeClaim, lo } return nil, nil } - - if !metav1.IsControlledBy(pod, pvc) { + if !metav1.IsControlledBy(pod, pvc) && !isImageStream(pvc) { return nil, errors.Errorf("Pod is not owned by PVC") } - log.V(1).Info("Pod is owned by PVC", pod.Name, pvc.Name) return pod, nil } @@ -301,7 +329,7 @@ func (r *ImportReconciler) reconcilePvc(pvc *corev1.PersistentVolumeClaim, log l } else { if pvc.DeletionTimestamp != nil { 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 } } else { @@ -411,7 +439,7 @@ func (r *ImportReconciler) updatePvcFromPod(pvc *corev1.PersistentVolumeClaim, p } if shouldDeletePod(pvc) { 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 } } @@ -419,6 +447,19 @@ func (r *ImportReconciler) updatePvcFromPod(pvc *corev1.PersistentVolumeClaim, p 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 { log.V(1).Info("Annotations are now", "pvc.anno", pvc.GetAnnotations()) if err := r.client.Update(context.TODO(), pvc); err != nil { @@ -459,12 +500,31 @@ func (r *ImportReconciler) createImporterPod(pvc *corev1.PersistentVolumeClaim) return err } // 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 { return err } 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 { r.log.V(1).Info("Pod requires scratch space") return r.createScratchPvcForPod(pvc, pod) @@ -638,7 +698,7 @@ func (r *ImportReconciler) requiresScratchSpace(pvc *corev1.PersistentVolumeClai case SourceGlance: scratchRequired = true case SourceRegistry: - scratchRequired = true + scratchRequired = pvc.Annotations[AnnRegistryImportMethod] != string(cdiv1.RegistryPullNode) } } value, ok := pvc.Annotations[AnnRequiresScratch] @@ -748,6 +808,22 @@ func getEndpoint(pvc *corev1.PersistentVolumeClaim) (string, error) { 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 func getValueFromAnnotation(pvc *corev1.PersistentVolumeClaim, annotation string) string { 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 // name, and pvc. A nil secret means the endpoint credentials are not passed to the // 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) { - podResourceRequirements, err := GetDefaultPodResourceRequirements(client) +func createImporterPod(log logr.Logger, client client.Client, args *importerPodArgs, installerLabels map[string]string) (*corev1.Pod, error) { + var err error + args.podResourceRequirements, err = GetDefaultPodResourceRequirements(client) if err != nil { return nil, err } - workloadNodePlacement, err := GetWorkloadNodePlacement(client) + args.workloadNodePlacement, err = GetWorkloadNodePlacement(client) if err != nil { 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") - if err := client.Create(context.TODO(), pod); err != nil { + if err = client.Create(context.TODO(), pod); err != nil { 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 } -// makeImporterPodSpec creates and return the importer pod spec based on the passed-in endpoint, secret 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 { +// makeNodeImporterPodSpec creates and returns the node docker cache based importer pod spec based on the passed-in importImage and pvc. +func makeNodeImporterPodSpec(args *importerPodArgs) *corev1.Pod { // 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 isController := true @@ -805,25 +1012,27 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar Name: DataVolName, VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: pvc.Name, + ClaimName: args.pvc.Name, ReadOnly: false, }, }, }, } - if scratchPvcName != nil { + if args.scratchPvcName != nil { volumes = append(volumes, corev1.Volume{ Name: ScratchVolName, VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: *scratchPvcName, + ClaimName: *args.scratchPvcName, ReadOnly: false, }, }, }) } + importerContainer := makeImporterContainerSpec(args.image, args.verbose, args.pullPolicy) + pod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", @@ -831,7 +1040,7 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar }, ObjectMeta: metav1.ObjectMeta{ Name: podName, - Namespace: namespace, + Namespace: args.pvc.Namespace, Annotations: map[string]string{ AnnCreatedBy: "yes", }, @@ -844,8 +1053,8 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar { APIVersion: "v1", Kind: "PersistentVolumeClaim", - Name: pvc.Name, - UID: pvc.GetUID(), + Name: args.pvc.Name, + UID: args.pvc.GetUID(), BlockOwnerDeletion: &blockOwnerDeletion, Controller: &isController, }, @@ -853,31 +1062,87 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar }, Spec: corev1.PodSpec{ Containers: []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, - }, - }, - }, + *importerContainer, }, RestartPolicy: corev1.RestartPolicyOnFailure, Volumes: volumes, - NodeSelector: workloadNodePlacement.NodeSelector, - Tolerations: workloadNodePlacement.Tolerations, - Affinity: workloadNodePlacement.Affinity, - PriorityClassName: priorityClassName, + NodeSelector: args.workloadNodePlacement.NodeSelector, + Tolerations: args.workloadNodePlacement.Tolerations, + Affinity: args.workloadNodePlacement.Affinity, + 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 { - pod.Spec.Containers[0].Resources = *podResourceRequirements + for i := range pod.Spec.Containers { + pod.Spec.Containers[i].Resources = *podResourceRequirements + } } ownerUID := pvc.UID @@ -894,68 +1159,8 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar 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) - 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) { // Set the fsGroup on the security context to the QemuSubGid if pod.Spec.SecurityContext == nil { @@ -965,7 +1170,22 @@ func makeImporterPodSpec(namespace, image, verbose, pullPolicy string, podEnvVar pod.Spec.SecurityContext.FSGroup = &fsGroup } 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 { @@ -1031,6 +1251,14 @@ func makeImportEnv(podEnvVar *importPodEnvVar, uid types.UID) []corev1.EnvVar { Name: common.ImporterUUID, Value: podEnvVar.uuid, }, + { + Name: common.ImporterReadyFile, + Value: podEnvVar.readyFile, + }, + { + Name: common.ImporterDoneFile, + Value: podEnvVar.doneFile, + }, { Name: common.ImporterBackingFile, Value: podEnvVar.backingFile, diff --git a/pkg/controller/import-controller_test.go b/pkg/controller/import-controller_test.go index db423549a..8df1e19bc 100644 --- a/pkg/controller/import-controller_test.go +++ b/pkg/controller/import-controller_test.go @@ -693,7 +693,16 @@ var _ = Describe("Create Importer Pod", func() { filesystemOverhead: "0.055", 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()) By("Verifying PVC owns pod") Expect(len(pod.GetOwnerReferences())).To(Equal(1)) @@ -748,6 +757,8 @@ var _ = Describe("Import test env", func() { certConfigMap: "", diskID: "", uuid: "", + readyFile: "", + doneFile: "", backingFile: "", thumbprint: "", filesystemOverhead: "0.055", @@ -993,6 +1004,14 @@ func createImportTestEnv(podEnvVar *importPodEnvVar, uid string) []corev1.EnvVar Name: common.ImporterUUID, Value: podEnvVar.uuid, }, + { + Name: common.ImporterReadyFile, + Value: podEnvVar.readyFile, + }, + { + Name: common.ImporterDoneFile, + Value: podEnvVar.doneFile, + }, { Name: common.ImporterBackingFile, Value: podEnvVar.backingFile, diff --git a/pkg/importer/transport.go b/pkg/importer/transport.go index 078781aef..e463956e2 100644 --- a/pkg/importer/transport.go +++ b/pkg/importer/transport.go @@ -31,6 +31,7 @@ import ( "github.com/containers/image/v5/types" "github.com/pkg/errors" "k8s.io/klog/v2" + cdiv1 "kubevirt.io/containerized-data-importer/pkg/apis/core/v1beta1" "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) } switch parts[0] { - case "docker": + case cdiv1.RegistrySchemeDocker: return docker.ParseReference(parts[1]) - case "oci-archive": + case cdiv1.RegistrySchemeOci: return archive.ParseReference(parts[1]) } return nil, errors.Errorf(`Invalid image name "%s", unknown transport`, img) diff --git a/pkg/operator/resources/crds_generated.go b/pkg/operator/resources/crds_generated.go index e007d22d3..752450ac3 100644 --- a/pkg/operator/resources/crds_generated.go +++ b/pkg/operator/resources/crds_generated.go @@ -2296,14 +2296,18 @@ spec: certConfigMap: description: CertConfigMap provides a reference to the Registry certs 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: description: SecretRef provides the secret reference needed to access the Registry source type: string 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 - required: - - url type: object required: - registry @@ -3083,14 +3087,18 @@ spec: certConfigMap: description: CertConfigMap provides a reference to the Registry certs 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: description: SecretRef provides the secret reference needed to access the Registry source type: string 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 - required: - - url type: object s3: description: DataVolumeSourceS3 provides the parameters to create a Data Volume from an S3 source diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 5e2ce5eb2..a7848211c 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -1,4 +1,6 @@ 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( name = "go_default_library", @@ -116,3 +118,16 @@ exports_files( "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"], +) diff --git a/tests/datavolume_test.go b/tests/datavolume_test.go index 1849b022e..2985462bc 100644 --- a/tests/datavolume_test.go +++ b/tests/datavolume_test.go @@ -104,7 +104,7 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests", dataVolume := utils.NewDataVolumeWithRegistryImport(dataVolumeName, size, url) cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs) Expect(err).To(BeNil()) - dataVolume.Spec.Source.Registry.CertConfigMap = cm + dataVolume.Spec.Source.Registry.CertConfigMap = &cm return dataVolume } @@ -112,7 +112,7 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests", dataVolume := utils.NewDataVolumeWithRegistryImport(dataVolumeName, size, url) cm, err := utils.CopyFileHostCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs) Expect(err).To(BeNil()) - dataVolume.Spec.Source.Registry.CertConfigMap = cm + dataVolume.Spec.Source.Registry.CertConfigMap = &cm return dataVolume } @@ -2151,13 +2151,13 @@ var _ = Describe("[vendor:cnv-qe@redhat.com][level:component]DataVolume tests", }) 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() { var pvc *v1.PersistentVolumeClaim 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) Expect(err).ToNot(HaveOccurred()) f.ForceBindPvcIfDvIsWaitForFirstConsumer(dataVolume) diff --git a/tests/framework/framework.go b/tests/framework/framework.go index aff2d610e..9c18a708c 100644 --- a/tests/framework/framework.go +++ b/tests/framework/framework.go @@ -77,6 +77,8 @@ type Clients struct { SnapshotSCName string BlockSCName string CsiCloneSCName string + DockerPrefix string + DockerTag string // k8sClient provides our k8s client pointer K8sClient *kubernetes.Clientset diff --git a/tests/import_proxy_test.go b/tests/import_proxy_test.go index a29892bff..74db0422a 100644 --- a/tests/import_proxy_test.go +++ b/tests/import_proxy_test.go @@ -184,7 +184,7 @@ var _ = Describe("Import Proxy tests", func() { dv := utils.NewDataVolumeWithRegistryImport("import-dv", "1Gi", fmt.Sprintf(utils.TinyCoreIsoRegistryURL, f.CdiInstallNs)) cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs) 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) Expect(err).To(BeNil()) dvName = dv.Name diff --git a/tests/import_test.go b/tests/import_test.go index 3dad122b3..d61a6cee6 100644 --- a/tests/import_test.go +++ b/tests/import_test.go @@ -1085,6 +1085,8 @@ var _ = Describe("Preallocation", func() { md5PrefixSize = int64(100000) config *cdiv1.CDIConfig 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() { @@ -1183,7 +1185,7 @@ var _ = Describe("Preallocation", func() { 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() By(fmt.Sprintf("Creating new datavolume %s", dv.Name)) preallocation := true @@ -1230,6 +1232,14 @@ var _ = Describe("Preallocation", func() { } else { 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 { return utils.NewDataVolumeWithHTTPImport("import-dv", "100Mi", tinyCoreIsoURL()) @@ -1279,7 +1289,23 @@ var _ = Describe("Preallocation", func() { dataVolume = utils.NewDataVolumeWithRegistryImport("import-dv", "100Mi", tinyCoreRegistryURL()) cm, err := utils.CopyRegistryCertConfigMap(f.K8sClient, f.Namespace.Name, f.CdiInstallNs) 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 }), Entry("VddkImport", true, utils.VcenterMD5, utils.DefaultImagePath, func() *cdiv1.DataVolume { diff --git a/tests/tests_suite_test.go b/tests/tests_suite_test.go index 9911ad57d..8008e3e3d 100644 --- a/tests/tests_suite_test.go +++ b/tests/tests_suite_test.go @@ -32,6 +32,8 @@ var ( snapshotSCName = flag.String("snapshot-sc", "", "The Storage Class supporting snapshots") blockSCName = flag.String("block-sc", "", "The Storage Class supporting block mode volumes") 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) { @@ -57,6 +59,8 @@ func BuildTestSuite() { framework.ClientsInstance.SnapshotSCName = *snapshotSCName framework.ClientsInstance.BlockSCName = *blockSCName 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, "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, "Block SC: %s\n", framework.ClientsInstance.BlockSCName) 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() if err != nil { diff --git a/tests/transport_test.go b/tests/transport_test.go index e5d1de50c..41b9b5b1f 100644 --- a/tests/transport_test.go +++ b/tests/transport_test.go @@ -2,12 +2,14 @@ package tests import ( "fmt" + . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" 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/controller" "kubevirt.io/containerized-data-importer/tests/framework" @@ -24,6 +26,7 @@ var _ = Describe("Transport Tests", func() { targetRawImage = "tinycoreqcow2" targetArchivedImage = "tinycoreisotar" targetArchivedImageHash = "b354a50183e70ee2ed3413eea67fe153" + targetNodePullImage = "cdi-func-test-tinycore" ) 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() // 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 ( 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{ - controller.AnnEndpoint: ep() + "/" + file, - controller.AnnSecret: "", - controller.AnnSource: source, + controller.AnnEndpoint: endpoint, + controller.AnnSecret: "", + controller.AnnSource: source, + controller.AnnRegistryImportMethod: registryImportMethod, } if accessKey != "" || secretKey != "" { @@ -127,7 +138,6 @@ var _ = Describe("Transport Tests", func() { }, timeout, pollingInterval).Should(BeTrue()) } } - httpNoAuthEp := func() string { 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) } 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) } + trustedRegistryEp := func() string { return fmt.Sprintf("docker://%s", f.DockerPrefix) } + 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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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: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), ) }) diff --git a/tests/utils/datavolume.go b/tests/utils/datavolume.go index 50bf59632..0fb5263f4 100644 --- a/tests/utils/datavolume.go +++ b/tests/utils/datavolume.go @@ -73,6 +73,10 @@ const ( ImageioImageURL = "https://imageio.%s:12345" // VcenterURL provides URL of vCenter/ESX simulator 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 = "3710416a680523c7d07538cb1026c60c" @@ -440,12 +444,13 @@ func NewDataVolumeForImageCloning(dataVolumeName, size, namespace, pvcName strin func NewDataVolumeWithRegistryImport(dataVolumeName string, size string, registryURL string) *cdiv1.DataVolume { return &cdiv1.DataVolume{ ObjectMeta: metav1.ObjectMeta{ - Name: dataVolumeName, + Name: dataVolumeName, + Annotations: map[string]string{}, }, Spec: cdiv1.DataVolumeSpec{ Source: &cdiv1.DataVolumeSource{ Registry: &cdiv1.DataVolumeSourceRegistry{ - URL: registryURL, + URL: ®istryURL, }, }, PVC: &k8sv1.PersistentVolumeClaimSpec{ diff --git a/tools/cdi-containerimage-server/BUILD.bazel b/tools/cdi-containerimage-server/BUILD.bazel new file mode 100644 index 000000000..3c33cb641 --- /dev/null +++ b/tools/cdi-containerimage-server/BUILD.bazel @@ -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"], +) diff --git a/tools/cdi-containerimage-server/main.go b/tools/cdi-containerimage-server/main.go new file mode 100755 index 000000000..5316213c1 --- /dev/null +++ b/tools/cdi-containerimage-server/main.go @@ -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") +}