mirror of
https://github.com/kubevirt/containerized-data-importer.git
synced 2025-06-03 06:30:22 +00:00
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:
parent
22afd303be
commit
addf25b4f9
@ -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",
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -89,6 +89,7 @@ container_image(
|
||||
],
|
||||
files = [
|
||||
":cdi-importer",
|
||||
"//tools/cdi-containerimage-server",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
@ -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"
|
||||
|
@ -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).
|
@ -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
|
||||
|
@ -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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,15 +240,74 @@ func (wh *dataVolumeValidatingWebhook) validateDataVolumeSpec(request *admission
|
||||
return causes
|
||||
}
|
||||
|
||||
if spec.Source.Registry != nil && spec.ContentType != "" && string(spec.ContentType) != string(cdiv1.DataVolumeKubeVirt) {
|
||||
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 " + string(cdiv1.DataVolumeKubeVirt) + " when Source is Registry"),
|
||||
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 {
|
||||
if spec.Source.Imageio.SecretRef == "" || spec.Source.Imageio.CertConfigMap == "" || spec.Source.Imageio.DiskID == "" {
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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,9 +439,22 @@ func (r *ImportReconciler) updatePvcFromPod(pvc *corev1.PersistentVolumeClaim, p
|
||||
}
|
||||
if shouldDeletePod(pvc) {
|
||||
log.V(1).Info("Deleting pod", "pod.Name", pod.Name)
|
||||
if err := r.cleanup(pvc, pod, log); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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"],
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.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),
|
||||
)
|
||||
})
|
||||
|
@ -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"
|
||||
@ -441,11 +445,12 @@ func NewDataVolumeWithRegistryImport(dataVolumeName string, size string, registr
|
||||
return &cdiv1.DataVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: dataVolumeName,
|
||||
Annotations: map[string]string{},
|
||||
},
|
||||
Spec: cdiv1.DataVolumeSpec{
|
||||
Source: &cdiv1.DataVolumeSource{
|
||||
Registry: &cdiv1.DataVolumeSourceRegistry{
|
||||
URL: registryURL,
|
||||
URL: ®istryURL,
|
||||
},
|
||||
},
|
||||
PVC: &k8sv1.PersistentVolumeClaimSpec{
|
||||
|
16
tools/cdi-containerimage-server/BUILD.bazel
Normal file
16
tools/cdi-containerimage-server/BUILD.bazel
Normal 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"],
|
||||
)
|
93
tools/cdi-containerimage-server/main.go
Executable file
93
tools/cdi-containerimage-server/main.go
Executable 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")
|
||||
}
|
Loading…
Reference in New Issue
Block a user