Skip to content

Commit b6b7468

Browse files
authored
VEP48: Support architecture specific image import for registry datasource (#3753)
* importer: introduce registry datasource architecture option This commit introduces preliminary support for a new optional field under the DV registry source. The field, platform, has an additional subfield: architecture which when specified serves as an image index filter to extract a disk.img only from layers which match it. If there's a mismatch between requested architecture and available architectures in the image index the import will fail. If the requested image is a manifest and not an index the architecture of the manifest will be compared to the requested architecture and if they mismatch the import will fail. API naming has been chosen to mimic the OCI image index spec[1]. [1] https://github.com/opencontainers/image-spec/blob/main/image-index.md#oci-image-index-specification Signed-off-by: Adi Aloni <[email protected]> * importer: pullMethod node architecture support This commit adds support for specifying architecture for the registry data source with pullMethod node. When configured, the importer Pod will gain another NodeSelector for nodes that have the matching architecture label. In the event that the importer Pod is unschedulable due to the pullMethod node's node selector, the condition will be propagated to the DV's running condition until it becomes schedulable. Signed-off-by: Adi Aloni <[email protected]> * docs, image-from-registry: add multi-platform support Adds a section in the docs about using the platform field for multi-platform image pull. Signed-off-by: Adi Aloni <[email protected]> --------- Signed-off-by: Adi Aloni <[email protected]>
1 parent 989b921 commit b6b7468

File tree

20 files changed

+392
-81
lines changed

20 files changed

+392
-81
lines changed

api/openapi-spec/swagger.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4866,6 +4866,10 @@
48664866
"description": "ImageStream is the name of image stream for import",
48674867
"type": "string"
48684868
},
4869+
"platform": {
4870+
"description": "Platform describes the minimum runtime requirements of the image",
4871+
"$ref": "#/definitions/v1beta1.PlatformOptions"
4872+
},
48694873
"pullMethod": {
48704874
"description": "PullMethod can be either \"pod\" (default import), or \"node\" (node docker cache based import)",
48714875
"type": "string"
@@ -5134,6 +5138,15 @@
51345138
"description": "OldTLSProfile is a TLS security profile based on: https://wiki.mozilla.org/Security/Server_Side_TLS#Old_backward_compatibility",
51355139
"type": "object"
51365140
},
5141+
"v1beta1.PlatformOptions": {
5142+
"type": "object",
5143+
"properties": {
5144+
"architecture": {
5145+
"description": "Architecture specifies the image target CPU architecture",
5146+
"type": "string"
5147+
}
5148+
}
5149+
},
51375150
"v1beta1.StorageSpec": {
51385151
"description": "StorageSpec defines the Storage type specification",
51395152
"type": "object",

cmd/cdi-importer/importer.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ func newDataSource(source string, contentType string, volumeMode v1.PersistentVo
262262
certDir, _ := util.ParseEnvVar(common.ImporterCertDirVar, false)
263263
insecureTLS, _ := strconv.ParseBool(os.Getenv(common.InsecureTLSVar))
264264
thumbprint, _ := util.ParseEnvVar(common.ImporterThumbprint, false)
265+
registryImageArchitecture, _ := util.ParseEnvVar(common.ImporterRegistryImageArchitecture, false)
265266

266267
currentCheckpoint, _ := util.ParseEnvVar(common.ImporterCurrentCheckpoint, false)
267268
previousCheckpoint, _ := util.ParseEnvVar(common.ImporterPreviousCheckpoint, false)
@@ -281,7 +282,7 @@ func newDataSource(source string, contentType string, volumeMode v1.PersistentVo
281282
}
282283
return ds
283284
case cc.SourceRegistry:
284-
ds := importer.NewRegistryDataSource(ep, acc, sec, certDir, insecureTLS)
285+
ds := importer.NewRegistryDataSource(ep, acc, sec, registryImageArchitecture, certDir, insecureTLS)
285286
return ds
286287
case cc.SourceS3:
287288
ds, err := importer.NewS3DataSource(ep, acc, sec, certDir)

doc/image-from-registry.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,31 @@ spec:
191191
```
192192

193193
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).
194+
195+
# Import registry image by platform specification
196+
197+
When importing an image from a [OCI Image Index](https://specs.opencontainers.org/image-spec/image-index/), you can optionally specify a `platform` field to influence which image variant is selected from the multi-platform manifest.
198+
Currently the `platform` field supports the following subfields for filtering:
199+
- `architecture` - Specifies the image target CPU architecture (e.g., `amd64`, `arm64`, `s390x`)
200+
201+
Subfields defined by the OCI Image Index `platform` specification but not listed above will default to the values defined in the OCI specification.
202+
203+
```yaml
204+
apiVersion: cdi.kubevirt.io/v1beta1
205+
kind: DataVolume
206+
metadata:
207+
name: registry-image-datavolume
208+
spec:
209+
source:
210+
registry:
211+
url: "docker://quay.io/containerdisks/fedora:latest"
212+
platform:
213+
architecture: "arm64"
214+
storage:
215+
resources:
216+
requests:
217+
storage: 10Gi
218+
```
219+
220+
> [!NOTE]
221+
> When `platform.architecture` is used together with `pullMethod: node`, a node selector will be added to the resulting importer Pod to ensure it schedules onto a node matching the specified architecture.

pkg/apis/core/v1beta1/openapi_generated.go

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/common/common.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ const (
163163
ImporterExtraHeader = "IMPORTER_EXTRA_HEADER_"
164164
// ImporterSecretExtraHeadersDir is where the secrets containing extra HTTP headers will be mounted
165165
ImporterSecretExtraHeadersDir = "/extraheaders"
166+
// ImporterRegistryImageArchitecture provides a constant to capture our env variable "IMPORTER_REGISTRY_IMAGE_ARCHITECTURE"
167+
ImporterRegistryImageArchitecture = "IMPORTER_REGISTRY_IMAGE_ARCHITECTURE"
166168

167169
// ImporterGoogleCredentialFileVar provides a constant to capture our env variable "GOOGLE_APPLICATION_CREDENTIALS"
168170
//nolint:gosec // This is not a real credential

pkg/controller/common/util.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ const (
8686
AnnPodReady = AnnAPIGroup + "/storage.pod.ready"
8787
// AnnPodRestarts is a PVC annotation that tells how many times a related pod was restarted
8888
AnnPodRestarts = AnnAPIGroup + "/storage.pod.restarts"
89+
// AnnPodSchedulable is a PVC annotation that tells if the Pod is schedulable or not
90+
AnnPodSchedulable = AnnAPIGroup + "/storage.pod.schedulable"
8991
// AnnPopulatedFor is a PVC annotation telling the datavolume controller that the PVC is already populated
9092
AnnPopulatedFor = AnnAPIGroup + "/storage.populatedFor"
9193
// AnnPrePopulated is a PVC annotation telling the datavolume controller that the PVC is already populated
@@ -188,6 +190,8 @@ const (
188190
AnnExtraHeaders = AnnAPIGroup + "/storage.import.extraHeaders"
189191
// AnnSecretExtraHeaders provides a const for our PVC secretExtraHeaders annotation
190192
AnnSecretExtraHeaders = AnnAPIGroup + "/storage.import.secretExtraHeaders"
193+
// AnnRegistryImageArchitecture provides a const for our PVC registryImageArchitecture annotation
194+
AnnRegistryImageArchitecture = AnnAPIGroup + "/storage.import.registryImageArchitecture"
191195

192196
// AnnCloneToken is the annotation containing the clone token
193197
AnnCloneToken = AnnAPIGroup + "/storage.clone.token"
@@ -1705,6 +1709,10 @@ func UpdateRegistryAnnotations(annotations map[string]string, registry *cdiv1.Da
17051709
if certConfigMap != nil && *certConfigMap != "" {
17061710
annotations[AnnCertConfigMap] = *certConfigMap
17071711
}
1712+
1713+
if registry.Platform != nil && registry.Platform.Architecture != "" {
1714+
annotations[AnnRegistryImageArchitecture] = registry.Platform.Architecture
1715+
}
17081716
}
17091717

17101718
// UpdateVDDKAnnotations updates the passed annotations for proper VDDK import

pkg/controller/datavolume/conditions.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ func updateRunningCondition(conditions []cdiv1.DataVolumeCondition, anno map[str
7575
default:
7676
conditions = updateCondition(conditions, cdiv1.DataVolumeRunning, corev1.ConditionUnknown, anno[cc.AnnRunningConditionMessage], anno[cc.AnnRunningConditionReason])
7777
}
78+
} else if schedulable, ok := anno[cc.AnnPodSchedulable]; ok && schedulable == "false" {
79+
conditions = updateCondition(conditions, cdiv1.DataVolumeRunning, corev1.ConditionFalse, "Importer pod cannot be scheduled", "Unschedulable")
7880
} else {
7981
conditions = updateCondition(conditions, cdiv1.DataVolumeRunning, corev1.ConditionFalse, anno[cc.AnnRunningConditionMessage], anno[cc.AnnRunningConditionReason])
8082
}

pkg/controller/import-controller.go

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -76,32 +76,33 @@ type ImportReconciler struct {
7676
}
7777

7878
type importPodEnvVar struct {
79-
ep string
80-
secretName string
81-
source string
82-
contentType string
83-
imageSize string
84-
certConfigMap string
85-
diskID string
86-
uuid string
87-
pullMethod string
88-
readyFile string
89-
doneFile string
90-
backingFile string
91-
thumbprint string
92-
filesystemOverhead string
93-
insecureTLS bool
94-
currentCheckpoint string
95-
previousCheckpoint string
96-
finalCheckpoint string
97-
preallocation bool
98-
httpProxy string
99-
httpsProxy string
100-
noProxy string
101-
certConfigMapProxy string
102-
extraHeaders []string
103-
secretExtraHeaders []string
104-
cacheMode string
79+
ep string
80+
secretName string
81+
source string
82+
contentType string
83+
imageSize string
84+
certConfigMap string
85+
diskID string
86+
uuid string
87+
pullMethod string
88+
readyFile string
89+
doneFile string
90+
backingFile string
91+
thumbprint string
92+
filesystemOverhead string
93+
insecureTLS bool
94+
currentCheckpoint string
95+
previousCheckpoint string
96+
finalCheckpoint string
97+
preallocation bool
98+
httpProxy string
99+
httpsProxy string
100+
noProxy string
101+
certConfigMapProxy string
102+
extraHeaders []string
103+
secretExtraHeaders []string
104+
cacheMode string
105+
registryImageArchitecture string
105106
}
106107

107108
type importerPodArgs struct {
@@ -405,6 +406,16 @@ func (r *ImportReconciler) updatePvcFromPod(pvc *corev1.PersistentVolumeClaim, p
405406
anno[cc.AnnPodPhase] = string(pod.Status.Phase)
406407
}
407408

409+
anno[cc.AnnPodSchedulable] = "true"
410+
if phase, ok := anno[cc.AnnPodPhase]; ok && phase == string(corev1.PodPending) {
411+
for _, cond := range pod.Status.Conditions {
412+
if cond.Type == corev1.PodScheduled && cond.Reason == corev1.PodReasonUnschedulable {
413+
anno[cc.AnnPodSchedulable] = "false"
414+
break
415+
}
416+
}
417+
}
418+
408419
for _, ev := range pod.Spec.Containers[0].Env {
409420
if ev.Name == common.CacheMode && ev.Value == common.CacheModeTryNone {
410421
anno[cc.AnnRequiresDirectIO] = "false"
@@ -599,6 +610,7 @@ func (r *ImportReconciler) createImportEnvVar(pvc *corev1.PersistentVolumeClaim)
599610
podEnvVar.previousCheckpoint = getValueFromAnnotation(pvc, cc.AnnPreviousCheckpoint)
600611
podEnvVar.currentCheckpoint = getValueFromAnnotation(pvc, cc.AnnCurrentCheckpoint)
601612
podEnvVar.finalCheckpoint = getValueFromAnnotation(pvc, cc.AnnFinalCheckpoint)
613+
podEnvVar.registryImageArchitecture = getValueFromAnnotation(pvc, cc.AnnRegistryImageArchitecture)
602614

603615
for annotation, value := range pvc.Annotations {
604616
if strings.HasPrefix(annotation, cc.AnnExtraHeaders) {
@@ -873,6 +885,9 @@ func createImporterPod(ctx context.Context, log logr.Logger, client client.Clien
873885
return nil, err
874886
}
875887
setRegistryNodeImportEnvVars(args)
888+
if args.podEnvVar.registryImageArchitecture != "" {
889+
setRegistryNodeImportNodeSelector(args)
890+
}
876891
}
877892

878893
pod := makeImporterPodSpec(args)
@@ -1181,6 +1196,13 @@ func setRegistryNodeImportEnvVars(args *importerPodArgs) {
11811196
args.podEnvVar.doneFile = "/shared/done"
11821197
}
11831198

1199+
func setRegistryNodeImportNodeSelector(args *importerPodArgs) {
1200+
if args.workloadNodePlacement.NodeSelector == nil {
1201+
args.workloadNodePlacement.NodeSelector = make(map[string]string, 0)
1202+
}
1203+
args.workloadNodePlacement.NodeSelector[v1.LabelArchStable] = args.podEnvVar.registryImageArchitecture
1204+
}
1205+
11841206
func createConfigMapVolume(certVolName, objRef string) corev1.Volume {
11851207
return corev1.Volume{
11861208
Name: certVolName,
@@ -1296,6 +1318,10 @@ func makeImportEnv(podEnvVar *importPodEnvVar, uid types.UID) []corev1.EnvVar {
12961318
Name: common.CacheMode,
12971319
Value: podEnvVar.cacheMode,
12981320
},
1321+
{
1322+
Name: common.ImporterRegistryImageArchitecture,
1323+
Value: podEnvVar.registryImageArchitecture,
1324+
},
12991325
}
13001326
if podEnvVar.secretName != "" && podEnvVar.source != cc.SourceGCS {
13011327
env = append(env, corev1.EnvVar{

pkg/controller/import-controller_test.go

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -997,27 +997,29 @@ var _ = Describe("Import test env", func() {
997997

998998
It("Should create import env", func() {
999999
testEnvVar := &importPodEnvVar{
1000-
ep: "myendpoint",
1001-
httpProxy: "httpproxy",
1002-
httpsProxy: "httpsproxy",
1003-
noProxy: "httpproxy",
1004-
secretName: "",
1005-
source: cc.SourceHTTP,
1006-
contentType: string(cdiv1.DataVolumeKubeVirt),
1007-
imageSize: "1G",
1008-
certConfigMap: "",
1009-
diskID: "",
1010-
uuid: "",
1011-
readyFile: "",
1012-
doneFile: "",
1013-
backingFile: "",
1014-
thumbprint: "",
1015-
filesystemOverhead: "0.055",
1016-
insecureTLS: false,
1017-
currentCheckpoint: "",
1018-
previousCheckpoint: "",
1019-
finalCheckpoint: "",
1020-
preallocation: false}
1000+
ep: "myendpoint",
1001+
httpProxy: "httpproxy",
1002+
httpsProxy: "httpsproxy",
1003+
noProxy: "httpproxy",
1004+
secretName: "",
1005+
source: cc.SourceHTTP,
1006+
contentType: string(cdiv1.DataVolumeKubeVirt),
1007+
imageSize: "1G",
1008+
certConfigMap: "",
1009+
diskID: "",
1010+
uuid: "",
1011+
readyFile: "",
1012+
doneFile: "",
1013+
backingFile: "",
1014+
thumbprint: "",
1015+
filesystemOverhead: "0.055",
1016+
insecureTLS: false,
1017+
currentCheckpoint: "",
1018+
previousCheckpoint: "",
1019+
finalCheckpoint: "",
1020+
preallocation: false,
1021+
registryImageArchitecture: "",
1022+
}
10211023
Expect(reflect.DeepEqual(makeImportEnv(testEnvVar, mockUID), createImportTestEnv(testEnvVar, mockUID))).To(BeTrue())
10221024
})
10231025
})
@@ -1300,6 +1302,10 @@ func createImportTestEnv(podEnvVar *importPodEnvVar, uid string) []corev1.EnvVar
13001302
Name: common.CacheMode,
13011303
Value: podEnvVar.cacheMode,
13021304
},
1305+
{
1306+
Name: common.ImporterRegistryImageArchitecture,
1307+
Value: podEnvVar.registryImageArchitecture,
1308+
},
13031309
}
13041310

13051311
if podEnvVar.secretName != "" {

pkg/controller/populators/populator-base.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ type updatePVCAnnotationsFunc func(pvc, pvcPrime *corev1.PersistentVolumeClaim)
227227

228228
var desiredAnnotations = []string{cc.AnnPodPhase, cc.AnnPodReady, cc.AnnPodRestarts,
229229
cc.AnnPreallocationRequested, cc.AnnPreallocationApplied, cc.AnnCurrentCheckpoint, cc.AnnMultiStageImportDone,
230-
cc.AnnRunningCondition, cc.AnnRunningConditionMessage, cc.AnnRunningConditionReason}
230+
cc.AnnRunningCondition, cc.AnnRunningConditionMessage, cc.AnnRunningConditionReason, cc.AnnPodSchedulable}
231231

232232
func (r *ReconcilerBase) updatePVCWithPVCPrimeAnnotations(pvc, pvcPrime *corev1.PersistentVolumeClaim, updateFunc updatePVCAnnotationsFunc) (*corev1.PersistentVolumeClaim, error) {
233233
pvcCopy := pvc.DeepCopy()

0 commit comments

Comments
 (0)